Rebuild admin pages around DataTable/AdminModal; restructure categories/groups and v2 groups API
Remove the generic $lib/svelte-admin CRUD layer and AdminTable.svelte; each admin route now owns its own form/modal logic. Add DataTable.svelte, superform-errors.ts, and profile-specific modal/field components. Flatten proclamation categories/groups and drop subscription levels; add the v2 groups API; update v1/v2 endpoints, serializers, auth, and regenerated Prisma client with the new migrations. Document the overhaul plan in docs/ADMIN_OVERHAUL.md.
This commit is contained in:
+2
-15
@@ -3,23 +3,10 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<script>
|
||||
(function () {
|
||||
const theme = localStorage.getItem('theme');
|
||||
if (
|
||||
theme === 'dark' ||
|
||||
(!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
<body class="m-0" data-sveltekit-preload-data="hover">
|
||||
<div class="flex h-dvh w-dvw flex-col">%sveltekit.body%</div>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div>%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+1
-1
@@ -51,6 +51,6 @@ export const auth = betterAuth({
|
||||
window: 60,
|
||||
max: 100
|
||||
},
|
||||
trustedOrigins: ['http://localhost:5173', 'https://portal.rhemawavetech.com'],
|
||||
trustedOrigins: ['http://localhost:5173', 'https://portal.radiantwavetech.com'],
|
||||
plugins: [sveltekitCookies(getRequestEvent)]
|
||||
});
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
<script lang="ts">
|
||||
export type DataTableColumn = {
|
||||
label: string;
|
||||
value: (record: Record<string, unknown>) => unknown;
|
||||
};
|
||||
|
||||
let {
|
||||
title = '',
|
||||
columns = [] as DataTableColumn[],
|
||||
records = [] as Record<string, unknown>[],
|
||||
newLabel = 'New',
|
||||
searchPlaceholder = 'Search...',
|
||||
copyableFields = [] as string[],
|
||||
draggable = false,
|
||||
onRowClick,
|
||||
onNew,
|
||||
onReorder
|
||||
}: {
|
||||
title?: string;
|
||||
columns?: DataTableColumn[];
|
||||
records?: Record<string, unknown>[];
|
||||
newLabel?: string;
|
||||
searchPlaceholder?: string;
|
||||
copyableFields?: string[];
|
||||
draggable?: boolean;
|
||||
onRowClick?: (record: Record<string, unknown>) => void;
|
||||
onNew?: () => void;
|
||||
onReorder?: (ids: (string | number)[]) => void;
|
||||
} = $props();
|
||||
|
||||
let search = $state('');
|
||||
|
||||
const filteredRecords = $derived(
|
||||
search
|
||||
? records.filter((record) =>
|
||||
columns.some((column) =>
|
||||
String(column.value(record) ?? '')
|
||||
.toLowerCase()
|
||||
.includes(search.toLowerCase())
|
||||
)
|
||||
)
|
||||
: records
|
||||
);
|
||||
|
||||
let dragOverride = $state<Record<string, unknown>[] | null>(null);
|
||||
let dragIndex = $state<number | null>(null);
|
||||
let hoverIndex = $state<number | null>(null);
|
||||
|
||||
const rows = $derived(dragOverride ?? filteredRecords);
|
||||
|
||||
function rowKey(record: Record<string, unknown>, index: number): string {
|
||||
return String(record.id ?? record.key ?? index);
|
||||
}
|
||||
|
||||
function copyValue(value: unknown) {
|
||||
navigator.clipboard.writeText(String(value ?? ''));
|
||||
}
|
||||
|
||||
function handleDragStart(e: DragEvent, i: number) {
|
||||
dragOverride = filteredRecords;
|
||||
dragIndex = i;
|
||||
hoverIndex = i;
|
||||
e.dataTransfer!.effectAllowed = 'move';
|
||||
e.dataTransfer!.setData('text/plain', '');
|
||||
}
|
||||
|
||||
function handleDragOver(e: DragEvent, i: number) {
|
||||
if (dragIndex === null) return;
|
||||
e.preventDefault();
|
||||
if (i === hoverIndex) return;
|
||||
e.dataTransfer!.dropEffect = 'move';
|
||||
|
||||
const reordered = rows.slice();
|
||||
const [moved] = reordered.splice(dragIndex, 1);
|
||||
reordered.splice(i, 0, moved);
|
||||
dragOverride = reordered;
|
||||
dragIndex = i;
|
||||
hoverIndex = i;
|
||||
}
|
||||
|
||||
async function handleDrop() {
|
||||
if (dragIndex === null) return;
|
||||
const ids = rows.map((r) => r.id as string | number);
|
||||
await onReorder?.(ids);
|
||||
dragIndex = null;
|
||||
hoverIndex = null;
|
||||
}
|
||||
|
||||
function handleDragEnd() {
|
||||
dragOverride = null;
|
||||
dragIndex = null;
|
||||
hoverIndex = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mx-auto w-full">
|
||||
<div class="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h2 class="text-xl font-semibold text-slate-900">{title}</h2>
|
||||
|
||||
<div class="flex w-full flex-col gap-3 sm:w-auto sm:flex-row sm:items-center">
|
||||
<input
|
||||
type="search"
|
||||
bind:value={search}
|
||||
placeholder={searchPlaceholder}
|
||||
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-indigo-500 focus:outline-none sm:w-64"
|
||||
/>
|
||||
{#if onNew}
|
||||
<button
|
||||
type="button"
|
||||
onclick={onNew}
|
||||
class="inline-flex h-10 items-center justify-center gap-1.5 rounded-md bg-indigo-600 px-4 text-sm font-medium text-white shadow-sm transition hover:bg-indigo-700"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"><path d="M12 5v14" /><path d="M5 12h14" /></svg
|
||||
>
|
||||
{newLabel}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full overflow-x-auto rounded-lg border border-slate-200 bg-white shadow-sm">
|
||||
<table class="w-full min-w-max text-left text-sm text-slate-700">
|
||||
<thead>
|
||||
<tr
|
||||
class="border-b border-slate-200 bg-slate-100 text-xs font-semibold tracking-wide text-slate-500"
|
||||
>
|
||||
{#if draggable}
|
||||
<th scope="col" class="w-10 px-4 py-3"></th>
|
||||
{/if}
|
||||
{#each columns as column (column.label)}
|
||||
<th scope="col" class="px-4 py-3">{column.label}</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each rows as record, i (rowKey(record, i))}
|
||||
<tr
|
||||
class="transition {i % 2 === 0 ? 'bg-white' : 'bg-slate-50'} {onRowClick
|
||||
? 'cursor-pointer hover:bg-indigo-50'
|
||||
: ''}"
|
||||
{draggable}
|
||||
ondragstart={(e) => handleDragStart(e, i)}
|
||||
ondragover={(e) => handleDragOver(e, i)}
|
||||
ondrop={handleDrop}
|
||||
ondragend={handleDragEnd}
|
||||
onclick={() => onRowClick?.(record)}
|
||||
>
|
||||
{#if draggable}
|
||||
<td class="px-4 py-3" onclick={(e) => e.stopPropagation()} aria-hidden="true">
|
||||
<span
|
||||
class="cursor-grab text-slate-400 select-none active:cursor-grabbing"
|
||||
title="Drag to reorder">⠿</span
|
||||
>
|
||||
</td>
|
||||
{/if}
|
||||
{#each columns as column (column.label)}
|
||||
<td class="px-4 py-3 whitespace-nowrap">
|
||||
{#if copyableFields.includes(column.label) && column.value(record) != null}
|
||||
<span>
|
||||
{String(column.value(record))}
|
||||
<button
|
||||
type="button"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
copyValue(column.value(record));
|
||||
}}
|
||||
title="Copy to clipboard"
|
||||
class="ml-1 inline-flex text-slate-400 transition hover:text-indigo-600"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="14"
|
||||
height="14"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect x="3.5" y="2.5" width="11" height="11" rx="1" />
|
||||
<path d="M5.5 2.5V1.5a1 1 0 011-1h3a1 1 0 011 1v1" />
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
{:else}
|
||||
{String(column.value(record) ?? '')}
|
||||
{/if}
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td
|
||||
colspan={columns.length + (draggable ? 1 : 0)}
|
||||
class="bg-white px-4 py-10 text-center text-slate-500"
|
||||
>
|
||||
No results found.
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -17,8 +17,8 @@
|
||||
deleteAction?: string;
|
||||
} = $props();
|
||||
|
||||
function handleBackdrop(e: MouseEvent) {
|
||||
if (e.target === e.currentTarget) onClose?.();
|
||||
function handleBackdrop() {
|
||||
onClose?.();
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
@@ -44,24 +44,27 @@
|
||||
|
||||
{#if open}
|
||||
<div
|
||||
role="presentation"
|
||||
class="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/50 pt-12"
|
||||
onclick={handleBackdrop}
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/50 p-4"
|
||||
onkeydown={handleKeydown}
|
||||
role="presentation"
|
||||
>
|
||||
<div class="absolute inset-0" onclick={handleBackdrop} aria-hidden="true"></div>
|
||||
<div
|
||||
class="my-8 w-full max-w-2xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={title}
|
||||
class="relative z-10 flex max-h-[90vh] w-full max-w-2xl flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-xl"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between border-b border-gray-200 px-6 py-4 dark:border-gray-700"
|
||||
class="flex shrink-0 items-center justify-between gap-3 border-b border-slate-200 px-6 py-4"
|
||||
>
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">{title}</h2>
|
||||
<h2 class="text-lg font-bold text-slate-900">{title}</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if canDelete && recordId}
|
||||
<button
|
||||
type="button"
|
||||
onclick={confirmDelete}
|
||||
class="px-3 py-1 text-sm text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
|
||||
class="rounded-md bg-red-50 px-3 py-1.5 text-sm font-medium text-red-600 ring-1 ring-red-200 transition ring-inset hover:bg-red-600 hover:text-white"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
@@ -69,12 +72,14 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={onClose}
|
||||
class="px-2 py-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>×</button
|
||||
aria-label="Close"
|
||||
class="rounded-md p-1.5 text-2xl leading-none text-slate-400 transition hover:bg-slate-100 hover:text-red-600"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<div class="overflow-y-auto px-6 py-5">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { PrismaScalarType } from '$lib/svelte-admin/types';
|
||||
import { fieldNameToLabel, resolveFieldValue } from '$lib/svelte-admin/introspect';
|
||||
|
||||
let {
|
||||
records = [],
|
||||
listFields = [],
|
||||
searchFields = [],
|
||||
fieldTypes = {} as Record<string, PrismaScalarType>,
|
||||
singular = 'record',
|
||||
draggable = false,
|
||||
copyableFields = [] as string[],
|
||||
onRowClick,
|
||||
onNew,
|
||||
onReorder
|
||||
}: {
|
||||
records: Record<string, unknown>[];
|
||||
listFields: string[];
|
||||
searchFields?: string[];
|
||||
fieldTypes?: Record<string, PrismaScalarType>;
|
||||
singular?: string;
|
||||
draggable?: boolean;
|
||||
copyableFields?: string[];
|
||||
onRowClick?: (record: Record<string, unknown>) => void;
|
||||
onNew?: () => void;
|
||||
onReorder?: (ids: (string | number)[]) => void;
|
||||
} = $props();
|
||||
|
||||
let search = $state('');
|
||||
let sortCol = $state<string | null>(null);
|
||||
let sortAsc = $state(true);
|
||||
|
||||
let filtered = $derived(
|
||||
search
|
||||
? records.filter((r) =>
|
||||
searchFields.some((f) => {
|
||||
const v = resolveFieldValue(r, f);
|
||||
return v != null && String(v).toLowerCase().includes(search.toLowerCase());
|
||||
})
|
||||
)
|
||||
: records
|
||||
);
|
||||
|
||||
let sorted = $derived(
|
||||
sortCol
|
||||
? [...filtered].sort((a, b) => {
|
||||
const va = resolveFieldValue(a, sortCol!);
|
||||
const vb = resolveFieldValue(b, sortCol!);
|
||||
if (va == null) return 1;
|
||||
if (vb == null) return -1;
|
||||
const cmp = String(va).localeCompare(String(vb), undefined, { numeric: true });
|
||||
return sortAsc ? cmp : -cmp;
|
||||
})
|
||||
: filtered
|
||||
);
|
||||
|
||||
let isDragging = $state(false);
|
||||
|
||||
let dragOverride = $state<Record<string, unknown>[] | null>(null);
|
||||
|
||||
let rows = $derived(dragOverride ?? sorted);
|
||||
|
||||
let dragIndex = $state<number | null>(null);
|
||||
let hoverIndex = $state<number | null>(null);
|
||||
|
||||
function toggleSort(field: string) {
|
||||
if (sortCol === field) {
|
||||
sortAsc = !sortAsc;
|
||||
} else {
|
||||
sortCol = field;
|
||||
sortAsc = true;
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragStart(e: DragEvent, i: number) {
|
||||
dragOverride = sorted;
|
||||
isDragging = true;
|
||||
dragIndex = i;
|
||||
hoverIndex = i;
|
||||
e.dataTransfer!.effectAllowed = 'move';
|
||||
e.dataTransfer!.setData('text/plain', '');
|
||||
}
|
||||
|
||||
function handleDragOver(e: DragEvent, i: number) {
|
||||
if (dragIndex === null) return;
|
||||
e.preventDefault();
|
||||
if (i === hoverIndex) return;
|
||||
e.dataTransfer!.dropEffect = 'move';
|
||||
|
||||
const reordered = rows.slice();
|
||||
const [moved] = reordered.splice(dragIndex, 1);
|
||||
reordered.splice(i, 0, moved);
|
||||
dragOverride = reordered;
|
||||
dragIndex = i;
|
||||
hoverIndex = i;
|
||||
}
|
||||
|
||||
async function handleDrop() {
|
||||
if (dragIndex === null) return;
|
||||
const ids = rows.map((r) => r.id as string | number);
|
||||
await onReorder?.(ids);
|
||||
isDragging = false;
|
||||
dragIndex = null;
|
||||
hoverIndex = null;
|
||||
}
|
||||
|
||||
function handleDragEnd() {
|
||||
dragOverride = null;
|
||||
isDragging = false;
|
||||
dragIndex = null;
|
||||
hoverIndex = null;
|
||||
}
|
||||
|
||||
function copyValue(v: unknown) {
|
||||
navigator.clipboard.writeText(String(v ?? ''));
|
||||
}
|
||||
|
||||
function formatCellValue(v: unknown, field: string): string {
|
||||
if (v == null) return '';
|
||||
const ft = fieldTypes[field];
|
||||
if (ft === 'DateTime') {
|
||||
if (v instanceof Date) return v.toLocaleString();
|
||||
return new Date(v as string).toLocaleString();
|
||||
}
|
||||
if (ft === 'Boolean') return v ? 'Active' : 'Inactive';
|
||||
if (ft === 'Float') {
|
||||
const n = Number(v);
|
||||
return Number.isNaN(n) ? String(v) : n.toFixed(2);
|
||||
}
|
||||
return String(v);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
{#if searchFields.length > 0}
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search {singular}s..."
|
||||
bind:value={search}
|
||||
class="max-w-xs border border-gray-300 px-3 py-1.5 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
{:else}
|
||||
<div></div>
|
||||
{/if}
|
||||
{#if onNew}
|
||||
<button
|
||||
type="button"
|
||||
onclick={onNew}
|
||||
class="border border-gray-300 px-3 py-1.5 text-sm font-medium dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
>
|
||||
New {singular} +
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto border border-gray-200 dark:border-gray-700">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-800">
|
||||
{#if draggable}
|
||||
<th scope="col" class="w-10 px-2 py-2"></th>
|
||||
{/if}
|
||||
{#each listFields as field (field)}
|
||||
<th
|
||||
scope="col"
|
||||
onclick={() => toggleSort(field)}
|
||||
class="cursor-pointer px-4 py-2 text-left text-xs font-medium tracking-wider text-gray-500 uppercase hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>
|
||||
{fieldNameToLabel(field)}
|
||||
{#if sortCol === field}
|
||||
<span class="ml-1">{sortAsc ? '\u25B2' : '\u25BC'}</span>
|
||||
{/if}
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each rows as record, i ((record['id'] as string) ?? (record['key'] as string) ?? i)}
|
||||
<tr
|
||||
{draggable}
|
||||
ondragstart={(e) => handleDragStart(e, i)}
|
||||
ondragover={(e) => handleDragOver(e, i)}
|
||||
ondrop={handleDrop}
|
||||
ondragend={handleDragEnd}
|
||||
onclick={() => onRowClick?.(record)}
|
||||
class="border-b border-gray-200 hover:bg-gray-100 dark:border-gray-700 dark:text-gray-100 dark:hover:bg-gray-800 {draggable &&
|
||||
isDragging &&
|
||||
dragIndex === i
|
||||
? 'opacity-40'
|
||||
: ''} {draggable && isDragging && hoverIndex === i
|
||||
? 'border-t-2 border-t-blue-500'
|
||||
: ''}"
|
||||
>
|
||||
{#if draggable}
|
||||
<td
|
||||
class="w-10 cursor-grab px-2 py-2 text-center"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<span class="text-gray-400 select-none">⠿</span>
|
||||
</td>
|
||||
{/if}
|
||||
{#each listFields as field (field)}
|
||||
<td class="px-4 py-2 text-sm">
|
||||
<span class="inline-flex items-center gap-1.5">
|
||||
{formatCellValue(resolveFieldValue(record, field), field)}
|
||||
{#if copyableFields.includes(field)}
|
||||
<button
|
||||
type="button"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
copyValue(resolveFieldValue(record, field));
|
||||
}}
|
||||
title="Copy to clipboard"
|
||||
class="shrink-0 leading-none text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
>
|
||||
<svg
|
||||
class="size-3.5"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect x="3.5" y="2.5" width="11" height="11" rx="1" />
|
||||
<path d="M5.5 2.5V1.5a1 1 0 011-1h3a1 1 0 011 1v1" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</span>
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td
|
||||
colspan={listFields.length + (draggable ? 1 : 0)}
|
||||
class="px-4 py-8 text-center text-sm text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
No {singular}s found.
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,11 +1,5 @@
|
||||
<script lang="ts">
|
||||
let {
|
||||
message = '',
|
||||
type = 'success' as 'success' | 'error'
|
||||
}: {
|
||||
message?: string;
|
||||
type?: 'success' | 'error';
|
||||
} = $props();
|
||||
let { message = '' }: { message?: string } = $props();
|
||||
|
||||
let remaining = $state(5);
|
||||
let visible = $state(true);
|
||||
@@ -32,15 +26,17 @@
|
||||
{#if visible}
|
||||
<div
|
||||
role="alert"
|
||||
class="fixed bottom-4 left-4 z-50 flex min-w-64 items-center gap-3 border px-4 py-3 text-sm shadow-lg {type ===
|
||||
'success'
|
||||
? 'border-green-600 bg-green-50 text-green-800'
|
||||
: 'border-red-600 bg-red-50 text-red-800'}"
|
||||
class="fixed right-4 bottom-4 z-50 flex items-center gap-3 rounded-lg border border-slate-700 bg-slate-900 px-4 py-3 text-sm text-white shadow-lg"
|
||||
>
|
||||
<span class="flex-1">{message}</span>
|
||||
<span class="text-xs tabular-nums opacity-70">{remaining}s</span>
|
||||
<button type="button" onclick={dismiss} class="ml-1 leading-none font-bold hover:opacity-70"
|
||||
>×</button
|
||||
<span>{message}</span>
|
||||
<span class="text-xs text-slate-400">{remaining}s</span>
|
||||
<button
|
||||
type="button"
|
||||
onclick={dismiss}
|
||||
aria-label="Dismiss"
|
||||
class="rounded p-1 text-slate-400 transition hover:text-white"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-0 flex-1 items-center justify-center overflow-auto">
|
||||
{@render children()}
|
||||
<div class="mx-auto w-full max-w-md px-4 py-8 sm:py-12">
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Editor } from '@tiptap/core';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import Highlight from '@tiptap/extension-highlight';
|
||||
import { TextStyle } from '@tiptap/extension-text-style';
|
||||
import { Color } from '@tiptap/extension-color';
|
||||
|
||||
let { initialContent = '', name = 'content' }: { initialContent?: string; name?: string } =
|
||||
$props();
|
||||
|
||||
let element: HTMLElement;
|
||||
let editor: Editor;
|
||||
let content = $state('');
|
||||
|
||||
onMount(() => {
|
||||
content = initialContent;
|
||||
|
||||
editor = new Editor({
|
||||
element,
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
bulletList: false,
|
||||
orderedList: false
|
||||
}),
|
||||
TextStyle,
|
||||
Color,
|
||||
Highlight.configure({ multicolor: true })
|
||||
],
|
||||
content: initialContent,
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class:
|
||||
'prose prose-sm max-w-none outline-none h-full min-h-40 p-3 dark:prose-invert dark:text-gray-100'
|
||||
}
|
||||
},
|
||||
onUpdate: ({ editor }) => {
|
||||
content = editor.getHTML();
|
||||
}
|
||||
});
|
||||
|
||||
editor.setEditable(true);
|
||||
|
||||
return () => editor.destroy();
|
||||
});
|
||||
|
||||
function toggleBold() {
|
||||
editor?.chain().focus().toggleBold().run();
|
||||
}
|
||||
function toggleItalic() {
|
||||
editor?.chain().focus().toggleItalic().run();
|
||||
}
|
||||
function toggleHighlight() {
|
||||
editor?.chain().focus().toggleHighlight().run();
|
||||
}
|
||||
|
||||
function setColor() {
|
||||
const current = editor?.getAttributes('textStyle').color ?? '#000000';
|
||||
const colors = ['#000000', '#FF0000', '#0000FF', '#008000', '#FFA500', '#800080', '#808080'];
|
||||
const idx = colors.indexOf(current);
|
||||
const next = colors[(idx + 1) % colors.length];
|
||||
editor?.chain().focus().setColor(next).run();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="border border-gray-300 dark:border-gray-600">
|
||||
<div
|
||||
class="flex gap-1 border-b border-gray-300 bg-gray-100 p-2 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onclick={toggleBold}
|
||||
class="min-w-8 px-2 py-1 font-bold hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
title="Bold">B</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onclick={toggleItalic}
|
||||
class="min-w-8 px-2 py-1 italic hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
title="Italic">I</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onclick={toggleHighlight}
|
||||
class="min-w-8 px-2 py-1 hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
title="Highlight"><span class="bg-yellow-200 px-1 dark:bg-yellow-600">H</span></button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onclick={setColor}
|
||||
class="min-w-8 px-2 py-1 hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
title="Cycle text color">A</button
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
bind:this={element}
|
||||
role="textbox"
|
||||
tabindex="0"
|
||||
contenteditable="true"
|
||||
class="min-h-40 cursor-text bg-white dark:bg-gray-900"
|
||||
></div>
|
||||
</div>
|
||||
<input type="hidden" {name} value={content} />
|
||||
@@ -57,6 +57,11 @@ export type Verification = Prisma.VerificationModel
|
||||
*
|
||||
*/
|
||||
export type Proclamation = Prisma.ProclamationModel
|
||||
/**
|
||||
* Model ProclamationGroup
|
||||
*
|
||||
*/
|
||||
export type ProclamationGroup = Prisma.ProclamationGroupModel
|
||||
/**
|
||||
* Model UserProclamations
|
||||
*
|
||||
@@ -72,11 +77,6 @@ export type SolfeggioFrequency = Prisma.SolfeggioFrequencyModel
|
||||
*
|
||||
*/
|
||||
export type ProclamationCategory = Prisma.ProclamationCategoryModel
|
||||
/**
|
||||
* Model ProclamationCategorySubscriptionLevel
|
||||
*
|
||||
*/
|
||||
export type ProclamationCategorySubscriptionLevel = Prisma.ProclamationCategorySubscriptionLevelModel
|
||||
/**
|
||||
* Model LicenseKey
|
||||
*
|
||||
@@ -87,11 +87,6 @@ export type LicenseKey = Prisma.LicenseKeyModel
|
||||
*
|
||||
*/
|
||||
export type ApiKey = Prisma.ApiKeyModel
|
||||
/**
|
||||
* Model SubscriptionLevel
|
||||
*
|
||||
*/
|
||||
export type SubscriptionLevel = Prisma.SubscriptionLevelModel
|
||||
/**
|
||||
* Model ColorPalette
|
||||
*
|
||||
|
||||
@@ -81,6 +81,11 @@ export type Verification = Prisma.VerificationModel
|
||||
*
|
||||
*/
|
||||
export type Proclamation = Prisma.ProclamationModel
|
||||
/**
|
||||
* Model ProclamationGroup
|
||||
*
|
||||
*/
|
||||
export type ProclamationGroup = Prisma.ProclamationGroupModel
|
||||
/**
|
||||
* Model UserProclamations
|
||||
*
|
||||
@@ -96,11 +101,6 @@ export type SolfeggioFrequency = Prisma.SolfeggioFrequencyModel
|
||||
*
|
||||
*/
|
||||
export type ProclamationCategory = Prisma.ProclamationCategoryModel
|
||||
/**
|
||||
* Model ProclamationCategorySubscriptionLevel
|
||||
*
|
||||
*/
|
||||
export type ProclamationCategorySubscriptionLevel = Prisma.ProclamationCategorySubscriptionLevelModel
|
||||
/**
|
||||
* Model LicenseKey
|
||||
*
|
||||
@@ -111,11 +111,6 @@ export type LicenseKey = Prisma.LicenseKeyModel
|
||||
*
|
||||
*/
|
||||
export type ApiKey = Prisma.ApiKeyModel
|
||||
/**
|
||||
* Model SubscriptionLevel
|
||||
*
|
||||
*/
|
||||
export type SubscriptionLevel = Prisma.SubscriptionLevelModel
|
||||
/**
|
||||
* Model ColorPalette
|
||||
*
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -392,13 +392,12 @@ export const ModelName = {
|
||||
UserRole: 'UserRole',
|
||||
Verification: 'Verification',
|
||||
Proclamation: 'Proclamation',
|
||||
ProclamationGroup: 'ProclamationGroup',
|
||||
UserProclamations: 'UserProclamations',
|
||||
SolfeggioFrequency: 'SolfeggioFrequency',
|
||||
ProclamationCategory: 'ProclamationCategory',
|
||||
ProclamationCategorySubscriptionLevel: 'ProclamationCategorySubscriptionLevel',
|
||||
LicenseKey: 'LicenseKey',
|
||||
ApiKey: 'ApiKey',
|
||||
SubscriptionLevel: 'SubscriptionLevel',
|
||||
ColorPalette: 'ColorPalette'
|
||||
} as const
|
||||
|
||||
@@ -415,7 +414,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
||||
omit: GlobalOmitOptions
|
||||
}
|
||||
meta: {
|
||||
modelProps: "user" | "profile" | "address" | "session" | "account" | "userRole" | "verification" | "proclamation" | "userProclamations" | "solfeggioFrequency" | "proclamationCategory" | "proclamationCategorySubscriptionLevel" | "licenseKey" | "apiKey" | "subscriptionLevel" | "colorPalette"
|
||||
modelProps: "user" | "profile" | "address" | "session" | "account" | "userRole" | "verification" | "proclamation" | "proclamationGroup" | "userProclamations" | "solfeggioFrequency" | "proclamationCategory" | "licenseKey" | "apiKey" | "colorPalette"
|
||||
txIsolationLevel: TransactionIsolationLevel
|
||||
}
|
||||
model: {
|
||||
@@ -1011,6 +1010,80 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
||||
}
|
||||
}
|
||||
}
|
||||
ProclamationGroup: {
|
||||
payload: Prisma.$ProclamationGroupPayload<ExtArgs>
|
||||
fields: Prisma.ProclamationGroupFieldRefs
|
||||
operations: {
|
||||
findUnique: {
|
||||
args: Prisma.ProclamationGroupFindUniqueArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload> | null
|
||||
}
|
||||
findUniqueOrThrow: {
|
||||
args: Prisma.ProclamationGroupFindUniqueOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>
|
||||
}
|
||||
findFirst: {
|
||||
args: Prisma.ProclamationGroupFindFirstArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload> | null
|
||||
}
|
||||
findFirstOrThrow: {
|
||||
args: Prisma.ProclamationGroupFindFirstOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>
|
||||
}
|
||||
findMany: {
|
||||
args: Prisma.ProclamationGroupFindManyArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>[]
|
||||
}
|
||||
create: {
|
||||
args: Prisma.ProclamationGroupCreateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>
|
||||
}
|
||||
createMany: {
|
||||
args: Prisma.ProclamationGroupCreateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
createManyAndReturn: {
|
||||
args: Prisma.ProclamationGroupCreateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>[]
|
||||
}
|
||||
delete: {
|
||||
args: Prisma.ProclamationGroupDeleteArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>
|
||||
}
|
||||
update: {
|
||||
args: Prisma.ProclamationGroupUpdateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>
|
||||
}
|
||||
deleteMany: {
|
||||
args: Prisma.ProclamationGroupDeleteManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateMany: {
|
||||
args: Prisma.ProclamationGroupUpdateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateManyAndReturn: {
|
||||
args: Prisma.ProclamationGroupUpdateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>[]
|
||||
}
|
||||
upsert: {
|
||||
args: Prisma.ProclamationGroupUpsertArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>
|
||||
}
|
||||
aggregate: {
|
||||
args: Prisma.ProclamationGroupAggregateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.AggregateProclamationGroup>
|
||||
}
|
||||
groupBy: {
|
||||
args: Prisma.ProclamationGroupGroupByArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.ProclamationGroupGroupByOutputType>[]
|
||||
}
|
||||
count: {
|
||||
args: Prisma.ProclamationGroupCountArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.ProclamationGroupCountAggregateOutputType> | number
|
||||
}
|
||||
}
|
||||
}
|
||||
UserProclamations: {
|
||||
payload: Prisma.$UserProclamationsPayload<ExtArgs>
|
||||
fields: Prisma.UserProclamationsFieldRefs
|
||||
@@ -1233,80 +1306,6 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
||||
}
|
||||
}
|
||||
}
|
||||
ProclamationCategorySubscriptionLevel: {
|
||||
payload: Prisma.$ProclamationCategorySubscriptionLevelPayload<ExtArgs>
|
||||
fields: Prisma.ProclamationCategorySubscriptionLevelFieldRefs
|
||||
operations: {
|
||||
findUnique: {
|
||||
args: Prisma.ProclamationCategorySubscriptionLevelFindUniqueArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload> | null
|
||||
}
|
||||
findUniqueOrThrow: {
|
||||
args: Prisma.ProclamationCategorySubscriptionLevelFindUniqueOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>
|
||||
}
|
||||
findFirst: {
|
||||
args: Prisma.ProclamationCategorySubscriptionLevelFindFirstArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload> | null
|
||||
}
|
||||
findFirstOrThrow: {
|
||||
args: Prisma.ProclamationCategorySubscriptionLevelFindFirstOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>
|
||||
}
|
||||
findMany: {
|
||||
args: Prisma.ProclamationCategorySubscriptionLevelFindManyArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>[]
|
||||
}
|
||||
create: {
|
||||
args: Prisma.ProclamationCategorySubscriptionLevelCreateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>
|
||||
}
|
||||
createMany: {
|
||||
args: Prisma.ProclamationCategorySubscriptionLevelCreateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
createManyAndReturn: {
|
||||
args: Prisma.ProclamationCategorySubscriptionLevelCreateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>[]
|
||||
}
|
||||
delete: {
|
||||
args: Prisma.ProclamationCategorySubscriptionLevelDeleteArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>
|
||||
}
|
||||
update: {
|
||||
args: Prisma.ProclamationCategorySubscriptionLevelUpdateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>
|
||||
}
|
||||
deleteMany: {
|
||||
args: Prisma.ProclamationCategorySubscriptionLevelDeleteManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateMany: {
|
||||
args: Prisma.ProclamationCategorySubscriptionLevelUpdateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateManyAndReturn: {
|
||||
args: Prisma.ProclamationCategorySubscriptionLevelUpdateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>[]
|
||||
}
|
||||
upsert: {
|
||||
args: Prisma.ProclamationCategorySubscriptionLevelUpsertArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>
|
||||
}
|
||||
aggregate: {
|
||||
args: Prisma.ProclamationCategorySubscriptionLevelAggregateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.AggregateProclamationCategorySubscriptionLevel>
|
||||
}
|
||||
groupBy: {
|
||||
args: Prisma.ProclamationCategorySubscriptionLevelGroupByArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.ProclamationCategorySubscriptionLevelGroupByOutputType>[]
|
||||
}
|
||||
count: {
|
||||
args: Prisma.ProclamationCategorySubscriptionLevelCountArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.ProclamationCategorySubscriptionLevelCountAggregateOutputType> | number
|
||||
}
|
||||
}
|
||||
}
|
||||
LicenseKey: {
|
||||
payload: Prisma.$LicenseKeyPayload<ExtArgs>
|
||||
fields: Prisma.LicenseKeyFieldRefs
|
||||
@@ -1455,80 +1454,6 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
||||
}
|
||||
}
|
||||
}
|
||||
SubscriptionLevel: {
|
||||
payload: Prisma.$SubscriptionLevelPayload<ExtArgs>
|
||||
fields: Prisma.SubscriptionLevelFieldRefs
|
||||
operations: {
|
||||
findUnique: {
|
||||
args: Prisma.SubscriptionLevelFindUniqueArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload> | null
|
||||
}
|
||||
findUniqueOrThrow: {
|
||||
args: Prisma.SubscriptionLevelFindUniqueOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>
|
||||
}
|
||||
findFirst: {
|
||||
args: Prisma.SubscriptionLevelFindFirstArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload> | null
|
||||
}
|
||||
findFirstOrThrow: {
|
||||
args: Prisma.SubscriptionLevelFindFirstOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>
|
||||
}
|
||||
findMany: {
|
||||
args: Prisma.SubscriptionLevelFindManyArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>[]
|
||||
}
|
||||
create: {
|
||||
args: Prisma.SubscriptionLevelCreateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>
|
||||
}
|
||||
createMany: {
|
||||
args: Prisma.SubscriptionLevelCreateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
createManyAndReturn: {
|
||||
args: Prisma.SubscriptionLevelCreateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>[]
|
||||
}
|
||||
delete: {
|
||||
args: Prisma.SubscriptionLevelDeleteArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>
|
||||
}
|
||||
update: {
|
||||
args: Prisma.SubscriptionLevelUpdateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>
|
||||
}
|
||||
deleteMany: {
|
||||
args: Prisma.SubscriptionLevelDeleteManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateMany: {
|
||||
args: Prisma.SubscriptionLevelUpdateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateManyAndReturn: {
|
||||
args: Prisma.SubscriptionLevelUpdateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>[]
|
||||
}
|
||||
upsert: {
|
||||
args: Prisma.SubscriptionLevelUpsertArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>
|
||||
}
|
||||
aggregate: {
|
||||
args: Prisma.SubscriptionLevelAggregateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.AggregateSubscriptionLevel>
|
||||
}
|
||||
groupBy: {
|
||||
args: Prisma.SubscriptionLevelGroupByArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.SubscriptionLevelGroupByOutputType>[]
|
||||
}
|
||||
count: {
|
||||
args: Prisma.SubscriptionLevelCountArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.SubscriptionLevelCountAggregateOutputType> | number
|
||||
}
|
||||
}
|
||||
}
|
||||
ColorPalette: {
|
||||
payload: Prisma.$ColorPalettePayload<ExtArgs>
|
||||
fields: Prisma.ColorPaletteFieldRefs
|
||||
@@ -1659,6 +1584,7 @@ export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof User
|
||||
export const ProfileScalarFieldEnum = {
|
||||
id: 'id',
|
||||
userId: 'userId',
|
||||
email: 'email',
|
||||
firstName: 'firstName',
|
||||
lastName: 'lastName',
|
||||
phoneNumber: 'phoneNumber',
|
||||
@@ -1744,12 +1670,21 @@ export const ProclamationScalarFieldEnum = {
|
||||
content: 'content',
|
||||
solfeggioFrequencyId: 'solfeggioFrequencyId',
|
||||
categoryId: 'categoryId',
|
||||
subscriptionLevelId: 'subscriptionLevelId'
|
||||
groupId: 'groupId'
|
||||
} as const
|
||||
|
||||
export type ProclamationScalarFieldEnum = (typeof ProclamationScalarFieldEnum)[keyof typeof ProclamationScalarFieldEnum]
|
||||
|
||||
|
||||
export const ProclamationGroupScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
sortOrder: 'sortOrder'
|
||||
} as const
|
||||
|
||||
export type ProclamationGroupScalarFieldEnum = (typeof ProclamationGroupScalarFieldEnum)[keyof typeof ProclamationGroupScalarFieldEnum]
|
||||
|
||||
|
||||
export const UserProclamationsScalarFieldEnum = {
|
||||
userId: 'userId',
|
||||
proclamationId: 'proclamationId',
|
||||
@@ -1772,27 +1707,18 @@ export type SolfeggioFrequencyScalarFieldEnum = (typeof SolfeggioFrequencyScalar
|
||||
export const ProclamationCategoryScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
sortOrder: 'sortOrder',
|
||||
parentId: 'parentId'
|
||||
sortOrder: 'sortOrder'
|
||||
} as const
|
||||
|
||||
export type ProclamationCategoryScalarFieldEnum = (typeof ProclamationCategoryScalarFieldEnum)[keyof typeof ProclamationCategoryScalarFieldEnum]
|
||||
|
||||
|
||||
export const ProclamationCategorySubscriptionLevelScalarFieldEnum = {
|
||||
categoryId: 'categoryId',
|
||||
subscriptionLevelId: 'subscriptionLevelId'
|
||||
} as const
|
||||
|
||||
export type ProclamationCategorySubscriptionLevelScalarFieldEnum = (typeof ProclamationCategorySubscriptionLevelScalarFieldEnum)[keyof typeof ProclamationCategorySubscriptionLevelScalarFieldEnum]
|
||||
|
||||
|
||||
export const LicenseKeyScalarFieldEnum = {
|
||||
id: 'id',
|
||||
key: 'key',
|
||||
active: 'active',
|
||||
level: 'level',
|
||||
userId: 'userId'
|
||||
userId: 'userId',
|
||||
email: 'email'
|
||||
} as const
|
||||
|
||||
export type LicenseKeyScalarFieldEnum = (typeof LicenseKeyScalarFieldEnum)[keyof typeof LicenseKeyScalarFieldEnum]
|
||||
@@ -1810,15 +1736,6 @@ export const ApiKeyScalarFieldEnum = {
|
||||
export type ApiKeyScalarFieldEnum = (typeof ApiKeyScalarFieldEnum)[keyof typeof ApiKeyScalarFieldEnum]
|
||||
|
||||
|
||||
export const SubscriptionLevelScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
description: 'description'
|
||||
} as const
|
||||
|
||||
export type SubscriptionLevelScalarFieldEnum = (typeof SubscriptionLevelScalarFieldEnum)[keyof typeof SubscriptionLevelScalarFieldEnum]
|
||||
|
||||
|
||||
export const ColorPaletteScalarFieldEnum = {
|
||||
solfeggioFrequencyId: 'solfeggioFrequencyId',
|
||||
colors: 'colors'
|
||||
@@ -2067,13 +1984,12 @@ export type GlobalOmitConfig = {
|
||||
userRole?: Prisma.UserRoleOmit
|
||||
verification?: Prisma.VerificationOmit
|
||||
proclamation?: Prisma.ProclamationOmit
|
||||
proclamationGroup?: Prisma.ProclamationGroupOmit
|
||||
userProclamations?: Prisma.UserProclamationsOmit
|
||||
solfeggioFrequency?: Prisma.SolfeggioFrequencyOmit
|
||||
proclamationCategory?: Prisma.ProclamationCategoryOmit
|
||||
proclamationCategorySubscriptionLevel?: Prisma.ProclamationCategorySubscriptionLevelOmit
|
||||
licenseKey?: Prisma.LicenseKeyOmit
|
||||
apiKey?: Prisma.ApiKeyOmit
|
||||
subscriptionLevel?: Prisma.SubscriptionLevelOmit
|
||||
colorPalette?: Prisma.ColorPaletteOmit
|
||||
}
|
||||
|
||||
|
||||
@@ -59,13 +59,12 @@ export const ModelName = {
|
||||
UserRole: 'UserRole',
|
||||
Verification: 'Verification',
|
||||
Proclamation: 'Proclamation',
|
||||
ProclamationGroup: 'ProclamationGroup',
|
||||
UserProclamations: 'UserProclamations',
|
||||
SolfeggioFrequency: 'SolfeggioFrequency',
|
||||
ProclamationCategory: 'ProclamationCategory',
|
||||
ProclamationCategorySubscriptionLevel: 'ProclamationCategorySubscriptionLevel',
|
||||
LicenseKey: 'LicenseKey',
|
||||
ApiKey: 'ApiKey',
|
||||
SubscriptionLevel: 'SubscriptionLevel',
|
||||
ColorPalette: 'ColorPalette'
|
||||
} as const
|
||||
|
||||
@@ -102,6 +101,7 @@ export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof User
|
||||
export const ProfileScalarFieldEnum = {
|
||||
id: 'id',
|
||||
userId: 'userId',
|
||||
email: 'email',
|
||||
firstName: 'firstName',
|
||||
lastName: 'lastName',
|
||||
phoneNumber: 'phoneNumber',
|
||||
@@ -187,12 +187,21 @@ export const ProclamationScalarFieldEnum = {
|
||||
content: 'content',
|
||||
solfeggioFrequencyId: 'solfeggioFrequencyId',
|
||||
categoryId: 'categoryId',
|
||||
subscriptionLevelId: 'subscriptionLevelId'
|
||||
groupId: 'groupId'
|
||||
} as const
|
||||
|
||||
export type ProclamationScalarFieldEnum = (typeof ProclamationScalarFieldEnum)[keyof typeof ProclamationScalarFieldEnum]
|
||||
|
||||
|
||||
export const ProclamationGroupScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
sortOrder: 'sortOrder'
|
||||
} as const
|
||||
|
||||
export type ProclamationGroupScalarFieldEnum = (typeof ProclamationGroupScalarFieldEnum)[keyof typeof ProclamationGroupScalarFieldEnum]
|
||||
|
||||
|
||||
export const UserProclamationsScalarFieldEnum = {
|
||||
userId: 'userId',
|
||||
proclamationId: 'proclamationId',
|
||||
@@ -215,27 +224,18 @@ export type SolfeggioFrequencyScalarFieldEnum = (typeof SolfeggioFrequencyScalar
|
||||
export const ProclamationCategoryScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
sortOrder: 'sortOrder',
|
||||
parentId: 'parentId'
|
||||
sortOrder: 'sortOrder'
|
||||
} as const
|
||||
|
||||
export type ProclamationCategoryScalarFieldEnum = (typeof ProclamationCategoryScalarFieldEnum)[keyof typeof ProclamationCategoryScalarFieldEnum]
|
||||
|
||||
|
||||
export const ProclamationCategorySubscriptionLevelScalarFieldEnum = {
|
||||
categoryId: 'categoryId',
|
||||
subscriptionLevelId: 'subscriptionLevelId'
|
||||
} as const
|
||||
|
||||
export type ProclamationCategorySubscriptionLevelScalarFieldEnum = (typeof ProclamationCategorySubscriptionLevelScalarFieldEnum)[keyof typeof ProclamationCategorySubscriptionLevelScalarFieldEnum]
|
||||
|
||||
|
||||
export const LicenseKeyScalarFieldEnum = {
|
||||
id: 'id',
|
||||
key: 'key',
|
||||
active: 'active',
|
||||
level: 'level',
|
||||
userId: 'userId'
|
||||
userId: 'userId',
|
||||
email: 'email'
|
||||
} as const
|
||||
|
||||
export type LicenseKeyScalarFieldEnum = (typeof LicenseKeyScalarFieldEnum)[keyof typeof LicenseKeyScalarFieldEnum]
|
||||
@@ -253,15 +253,6 @@ export const ApiKeyScalarFieldEnum = {
|
||||
export type ApiKeyScalarFieldEnum = (typeof ApiKeyScalarFieldEnum)[keyof typeof ApiKeyScalarFieldEnum]
|
||||
|
||||
|
||||
export const SubscriptionLevelScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
description: 'description'
|
||||
} as const
|
||||
|
||||
export type SubscriptionLevelScalarFieldEnum = (typeof SubscriptionLevelScalarFieldEnum)[keyof typeof SubscriptionLevelScalarFieldEnum]
|
||||
|
||||
|
||||
export const ColorPaletteScalarFieldEnum = {
|
||||
solfeggioFrequencyId: 'solfeggioFrequencyId',
|
||||
colors: 'colors'
|
||||
|
||||
@@ -16,12 +16,11 @@ export type * from './models/Account.ts'
|
||||
export type * from './models/UserRole.ts'
|
||||
export type * from './models/Verification.ts'
|
||||
export type * from './models/Proclamation.ts'
|
||||
export type * from './models/ProclamationGroup.ts'
|
||||
export type * from './models/UserProclamations.ts'
|
||||
export type * from './models/SolfeggioFrequency.ts'
|
||||
export type * from './models/ProclamationCategory.ts'
|
||||
export type * from './models/ProclamationCategorySubscriptionLevel.ts'
|
||||
export type * from './models/LicenseKey.ts'
|
||||
export type * from './models/ApiKey.ts'
|
||||
export type * from './models/SubscriptionLevel.ts'
|
||||
export type * from './models/ColorPalette.ts'
|
||||
export type * from './commonInputTypes.ts'
|
||||
@@ -28,24 +28,24 @@ export type LicenseKeyMinAggregateOutputType = {
|
||||
id: string | null
|
||||
key: string | null
|
||||
active: boolean | null
|
||||
level: string | null
|
||||
userId: string | null
|
||||
email: string | null
|
||||
}
|
||||
|
||||
export type LicenseKeyMaxAggregateOutputType = {
|
||||
id: string | null
|
||||
key: string | null
|
||||
active: boolean | null
|
||||
level: string | null
|
||||
userId: string | null
|
||||
email: string | null
|
||||
}
|
||||
|
||||
export type LicenseKeyCountAggregateOutputType = {
|
||||
id: number
|
||||
key: number
|
||||
active: number
|
||||
level: number
|
||||
userId: number
|
||||
email: number
|
||||
_all: number
|
||||
}
|
||||
|
||||
@@ -54,24 +54,24 @@ export type LicenseKeyMinAggregateInputType = {
|
||||
id?: true
|
||||
key?: true
|
||||
active?: true
|
||||
level?: true
|
||||
userId?: true
|
||||
email?: true
|
||||
}
|
||||
|
||||
export type LicenseKeyMaxAggregateInputType = {
|
||||
id?: true
|
||||
key?: true
|
||||
active?: true
|
||||
level?: true
|
||||
userId?: true
|
||||
email?: true
|
||||
}
|
||||
|
||||
export type LicenseKeyCountAggregateInputType = {
|
||||
id?: true
|
||||
key?: true
|
||||
active?: true
|
||||
level?: true
|
||||
userId?: true
|
||||
email?: true
|
||||
_all?: true
|
||||
}
|
||||
|
||||
@@ -151,8 +151,8 @@ export type LicenseKeyGroupByOutputType = {
|
||||
id: string
|
||||
key: string
|
||||
active: boolean
|
||||
level: string
|
||||
userId: string
|
||||
userId: string | null
|
||||
email: string | null
|
||||
_count: LicenseKeyCountAggregateOutputType | null
|
||||
_min: LicenseKeyMinAggregateOutputType | null
|
||||
_max: LicenseKeyMaxAggregateOutputType | null
|
||||
@@ -180,17 +180,17 @@ export type LicenseKeyWhereInput = {
|
||||
id?: Prisma.StringFilter<"LicenseKey"> | string
|
||||
key?: Prisma.StringFilter<"LicenseKey"> | string
|
||||
active?: Prisma.BoolFilter<"LicenseKey"> | boolean
|
||||
level?: Prisma.StringFilter<"LicenseKey"> | string
|
||||
userId?: Prisma.StringFilter<"LicenseKey"> | string
|
||||
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
|
||||
userId?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
|
||||
email?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
|
||||
user?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
|
||||
}
|
||||
|
||||
export type LicenseKeyOrderByWithRelationInput = {
|
||||
id?: Prisma.SortOrder
|
||||
key?: Prisma.SortOrder
|
||||
active?: Prisma.SortOrder
|
||||
level?: Prisma.SortOrder
|
||||
userId?: Prisma.SortOrder
|
||||
userId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
email?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
user?: Prisma.UserOrderByWithRelationInput
|
||||
}
|
||||
|
||||
@@ -201,17 +201,17 @@ export type LicenseKeyWhereUniqueInput = Prisma.AtLeast<{
|
||||
OR?: Prisma.LicenseKeyWhereInput[]
|
||||
NOT?: Prisma.LicenseKeyWhereInput | Prisma.LicenseKeyWhereInput[]
|
||||
active?: Prisma.BoolFilter<"LicenseKey"> | boolean
|
||||
level?: Prisma.StringFilter<"LicenseKey"> | string
|
||||
userId?: Prisma.StringFilter<"LicenseKey"> | string
|
||||
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
|
||||
userId?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
|
||||
email?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
|
||||
user?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
|
||||
}, "id" | "key">
|
||||
|
||||
export type LicenseKeyOrderByWithAggregationInput = {
|
||||
id?: Prisma.SortOrder
|
||||
key?: Prisma.SortOrder
|
||||
active?: Prisma.SortOrder
|
||||
level?: Prisma.SortOrder
|
||||
userId?: Prisma.SortOrder
|
||||
userId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
email?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
_count?: Prisma.LicenseKeyCountOrderByAggregateInput
|
||||
_max?: Prisma.LicenseKeyMaxOrderByAggregateInput
|
||||
_min?: Prisma.LicenseKeyMinOrderByAggregateInput
|
||||
@@ -224,63 +224,63 @@ export type LicenseKeyScalarWhereWithAggregatesInput = {
|
||||
id?: Prisma.StringWithAggregatesFilter<"LicenseKey"> | string
|
||||
key?: Prisma.StringWithAggregatesFilter<"LicenseKey"> | string
|
||||
active?: Prisma.BoolWithAggregatesFilter<"LicenseKey"> | boolean
|
||||
level?: Prisma.StringWithAggregatesFilter<"LicenseKey"> | string
|
||||
userId?: Prisma.StringWithAggregatesFilter<"LicenseKey"> | string
|
||||
userId?: Prisma.StringNullableWithAggregatesFilter<"LicenseKey"> | string | null
|
||||
email?: Prisma.StringNullableWithAggregatesFilter<"LicenseKey"> | string | null
|
||||
}
|
||||
|
||||
export type LicenseKeyCreateInput = {
|
||||
id?: string
|
||||
key: string
|
||||
active: boolean
|
||||
level: string
|
||||
user: Prisma.UserCreateNestedOneWithoutLicenseKeysInput
|
||||
email?: string | null
|
||||
user?: Prisma.UserCreateNestedOneWithoutLicenseKeysInput
|
||||
}
|
||||
|
||||
export type LicenseKeyUncheckedCreateInput = {
|
||||
id?: string
|
||||
key: string
|
||||
active: boolean
|
||||
level: string
|
||||
userId: string
|
||||
userId?: string | null
|
||||
email?: string | null
|
||||
}
|
||||
|
||||
export type LicenseKeyUpdateInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
key?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||
level?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
user?: Prisma.UserUpdateOneRequiredWithoutLicenseKeysNestedInput
|
||||
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
user?: Prisma.UserUpdateOneWithoutLicenseKeysNestedInput
|
||||
}
|
||||
|
||||
export type LicenseKeyUncheckedUpdateInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
key?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||
level?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
}
|
||||
|
||||
export type LicenseKeyCreateManyInput = {
|
||||
id?: string
|
||||
key: string
|
||||
active: boolean
|
||||
level: string
|
||||
userId: string
|
||||
userId?: string | null
|
||||
email?: string | null
|
||||
}
|
||||
|
||||
export type LicenseKeyUpdateManyMutationInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
key?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||
level?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
}
|
||||
|
||||
export type LicenseKeyUncheckedUpdateManyInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
key?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||
level?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
}
|
||||
|
||||
export type LicenseKeyListRelationFilter = {
|
||||
@@ -297,24 +297,24 @@ export type LicenseKeyCountOrderByAggregateInput = {
|
||||
id?: Prisma.SortOrder
|
||||
key?: Prisma.SortOrder
|
||||
active?: Prisma.SortOrder
|
||||
level?: Prisma.SortOrder
|
||||
userId?: Prisma.SortOrder
|
||||
email?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type LicenseKeyMaxOrderByAggregateInput = {
|
||||
id?: Prisma.SortOrder
|
||||
key?: Prisma.SortOrder
|
||||
active?: Prisma.SortOrder
|
||||
level?: Prisma.SortOrder
|
||||
userId?: Prisma.SortOrder
|
||||
email?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type LicenseKeyMinOrderByAggregateInput = {
|
||||
id?: Prisma.SortOrder
|
||||
key?: Prisma.SortOrder
|
||||
active?: Prisma.SortOrder
|
||||
level?: Prisma.SortOrder
|
||||
userId?: Prisma.SortOrder
|
||||
email?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type LicenseKeyCreateNestedManyWithoutUserInput = {
|
||||
@@ -363,14 +363,14 @@ export type LicenseKeyCreateWithoutUserInput = {
|
||||
id?: string
|
||||
key: string
|
||||
active: boolean
|
||||
level: string
|
||||
email?: string | null
|
||||
}
|
||||
|
||||
export type LicenseKeyUncheckedCreateWithoutUserInput = {
|
||||
id?: string
|
||||
key: string
|
||||
active: boolean
|
||||
level: string
|
||||
email?: string | null
|
||||
}
|
||||
|
||||
export type LicenseKeyCreateOrConnectWithoutUserInput = {
|
||||
@@ -406,36 +406,36 @@ export type LicenseKeyScalarWhereInput = {
|
||||
id?: Prisma.StringFilter<"LicenseKey"> | string
|
||||
key?: Prisma.StringFilter<"LicenseKey"> | string
|
||||
active?: Prisma.BoolFilter<"LicenseKey"> | boolean
|
||||
level?: Prisma.StringFilter<"LicenseKey"> | string
|
||||
userId?: Prisma.StringFilter<"LicenseKey"> | string
|
||||
userId?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
|
||||
email?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
|
||||
}
|
||||
|
||||
export type LicenseKeyCreateManyUserInput = {
|
||||
id?: string
|
||||
key: string
|
||||
active: boolean
|
||||
level: string
|
||||
email?: string | null
|
||||
}
|
||||
|
||||
export type LicenseKeyUpdateWithoutUserInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
key?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||
level?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
}
|
||||
|
||||
export type LicenseKeyUncheckedUpdateWithoutUserInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
key?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||
level?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
}
|
||||
|
||||
export type LicenseKeyUncheckedUpdateManyWithoutUserInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
key?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||
level?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
}
|
||||
|
||||
|
||||
@@ -444,59 +444,59 @@ export type LicenseKeySelect<ExtArgs extends runtime.Types.Extensions.InternalAr
|
||||
id?: boolean
|
||||
key?: boolean
|
||||
active?: boolean
|
||||
level?: boolean
|
||||
userId?: boolean
|
||||
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
||||
email?: boolean
|
||||
user?: boolean | Prisma.LicenseKey$userArgs<ExtArgs>
|
||||
}, ExtArgs["result"]["licenseKey"]>
|
||||
|
||||
export type LicenseKeySelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||
id?: boolean
|
||||
key?: boolean
|
||||
active?: boolean
|
||||
level?: boolean
|
||||
userId?: boolean
|
||||
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
||||
email?: boolean
|
||||
user?: boolean | Prisma.LicenseKey$userArgs<ExtArgs>
|
||||
}, ExtArgs["result"]["licenseKey"]>
|
||||
|
||||
export type LicenseKeySelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||
id?: boolean
|
||||
key?: boolean
|
||||
active?: boolean
|
||||
level?: boolean
|
||||
userId?: boolean
|
||||
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
||||
email?: boolean
|
||||
user?: boolean | Prisma.LicenseKey$userArgs<ExtArgs>
|
||||
}, ExtArgs["result"]["licenseKey"]>
|
||||
|
||||
export type LicenseKeySelectScalar = {
|
||||
id?: boolean
|
||||
key?: boolean
|
||||
active?: boolean
|
||||
level?: boolean
|
||||
userId?: boolean
|
||||
email?: boolean
|
||||
}
|
||||
|
||||
export type LicenseKeyOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "key" | "active" | "level" | "userId", ExtArgs["result"]["licenseKey"]>
|
||||
export type LicenseKeyOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "key" | "active" | "userId" | "email", ExtArgs["result"]["licenseKey"]>
|
||||
export type LicenseKeyInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
||||
user?: boolean | Prisma.LicenseKey$userArgs<ExtArgs>
|
||||
}
|
||||
export type LicenseKeyIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
||||
user?: boolean | Prisma.LicenseKey$userArgs<ExtArgs>
|
||||
}
|
||||
export type LicenseKeyIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
||||
user?: boolean | Prisma.LicenseKey$userArgs<ExtArgs>
|
||||
}
|
||||
|
||||
export type $LicenseKeyPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
name: "LicenseKey"
|
||||
objects: {
|
||||
user: Prisma.$UserPayload<ExtArgs>
|
||||
user: Prisma.$UserPayload<ExtArgs> | null
|
||||
}
|
||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||
id: string
|
||||
key: string
|
||||
active: boolean
|
||||
level: string
|
||||
userId: string
|
||||
userId: string | null
|
||||
email: string | null
|
||||
}, ExtArgs["result"]["licenseKey"]>
|
||||
composites: {}
|
||||
}
|
||||
@@ -891,7 +891,7 @@ readonly fields: LicenseKeyFieldRefs;
|
||||
*/
|
||||
export interface Prisma__LicenseKeyClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
|
||||
readonly [Symbol.toStringTag]: "PrismaPromise"
|
||||
user<T extends Prisma.UserDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.UserDefaultArgs<ExtArgs>>): Prisma.Prisma__UserClient<runtime.Types.Result.GetResult<Prisma.$UserPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
|
||||
user<T extends Prisma.LicenseKey$userArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.LicenseKey$userArgs<ExtArgs>>): Prisma.Prisma__UserClient<runtime.Types.Result.GetResult<Prisma.$UserPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
||||
/**
|
||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
||||
@@ -924,8 +924,8 @@ export interface LicenseKeyFieldRefs {
|
||||
readonly id: Prisma.FieldRef<"LicenseKey", 'String'>
|
||||
readonly key: Prisma.FieldRef<"LicenseKey", 'String'>
|
||||
readonly active: Prisma.FieldRef<"LicenseKey", 'Boolean'>
|
||||
readonly level: Prisma.FieldRef<"LicenseKey", 'String'>
|
||||
readonly userId: Prisma.FieldRef<"LicenseKey", 'String'>
|
||||
readonly email: Prisma.FieldRef<"LicenseKey", 'String'>
|
||||
}
|
||||
|
||||
|
||||
@@ -1326,6 +1326,25 @@ export type LicenseKeyDeleteManyArgs<ExtArgs extends runtime.Types.Extensions.In
|
||||
limit?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* LicenseKey.user
|
||||
*/
|
||||
export type LicenseKey$userArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
/**
|
||||
* Select specific fields to fetch from the User
|
||||
*/
|
||||
select?: Prisma.UserSelect<ExtArgs> | null
|
||||
/**
|
||||
* Omit specific fields from the User
|
||||
*/
|
||||
omit?: Prisma.UserOmit<ExtArgs> | null
|
||||
/**
|
||||
* Choose, which related nodes to fetch as well
|
||||
*/
|
||||
include?: Prisma.UserInclude<ExtArgs> | null
|
||||
where?: Prisma.UserWhereInput
|
||||
}
|
||||
|
||||
/**
|
||||
* LicenseKey without action
|
||||
*/
|
||||
|
||||
@@ -29,13 +29,13 @@ export type AggregateProclamation = {
|
||||
export type ProclamationAvgAggregateOutputType = {
|
||||
solfeggioFrequencyId: number | null
|
||||
categoryId: number | null
|
||||
subscriptionLevelId: number | null
|
||||
groupId: number | null
|
||||
}
|
||||
|
||||
export type ProclamationSumAggregateOutputType = {
|
||||
solfeggioFrequencyId: number | null
|
||||
categoryId: number | null
|
||||
subscriptionLevelId: number | null
|
||||
groupId: number | null
|
||||
}
|
||||
|
||||
export type ProclamationMinAggregateOutputType = {
|
||||
@@ -44,7 +44,7 @@ export type ProclamationMinAggregateOutputType = {
|
||||
content: string | null
|
||||
solfeggioFrequencyId: number | null
|
||||
categoryId: number | null
|
||||
subscriptionLevelId: number | null
|
||||
groupId: number | null
|
||||
}
|
||||
|
||||
export type ProclamationMaxAggregateOutputType = {
|
||||
@@ -53,7 +53,7 @@ export type ProclamationMaxAggregateOutputType = {
|
||||
content: string | null
|
||||
solfeggioFrequencyId: number | null
|
||||
categoryId: number | null
|
||||
subscriptionLevelId: number | null
|
||||
groupId: number | null
|
||||
}
|
||||
|
||||
export type ProclamationCountAggregateOutputType = {
|
||||
@@ -62,7 +62,7 @@ export type ProclamationCountAggregateOutputType = {
|
||||
content: number
|
||||
solfeggioFrequencyId: number
|
||||
categoryId: number
|
||||
subscriptionLevelId: number
|
||||
groupId: number
|
||||
_all: number
|
||||
}
|
||||
|
||||
@@ -70,13 +70,13 @@ export type ProclamationCountAggregateOutputType = {
|
||||
export type ProclamationAvgAggregateInputType = {
|
||||
solfeggioFrequencyId?: true
|
||||
categoryId?: true
|
||||
subscriptionLevelId?: true
|
||||
groupId?: true
|
||||
}
|
||||
|
||||
export type ProclamationSumAggregateInputType = {
|
||||
solfeggioFrequencyId?: true
|
||||
categoryId?: true
|
||||
subscriptionLevelId?: true
|
||||
groupId?: true
|
||||
}
|
||||
|
||||
export type ProclamationMinAggregateInputType = {
|
||||
@@ -85,7 +85,7 @@ export type ProclamationMinAggregateInputType = {
|
||||
content?: true
|
||||
solfeggioFrequencyId?: true
|
||||
categoryId?: true
|
||||
subscriptionLevelId?: true
|
||||
groupId?: true
|
||||
}
|
||||
|
||||
export type ProclamationMaxAggregateInputType = {
|
||||
@@ -94,7 +94,7 @@ export type ProclamationMaxAggregateInputType = {
|
||||
content?: true
|
||||
solfeggioFrequencyId?: true
|
||||
categoryId?: true
|
||||
subscriptionLevelId?: true
|
||||
groupId?: true
|
||||
}
|
||||
|
||||
export type ProclamationCountAggregateInputType = {
|
||||
@@ -103,7 +103,7 @@ export type ProclamationCountAggregateInputType = {
|
||||
content?: true
|
||||
solfeggioFrequencyId?: true
|
||||
categoryId?: true
|
||||
subscriptionLevelId?: true
|
||||
groupId?: true
|
||||
_all?: true
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ export type ProclamationGroupByOutputType = {
|
||||
content: string
|
||||
solfeggioFrequencyId: number
|
||||
categoryId: number
|
||||
subscriptionLevelId: number
|
||||
groupId: number
|
||||
_count: ProclamationCountAggregateOutputType | null
|
||||
_avg: ProclamationAvgAggregateOutputType | null
|
||||
_sum: ProclamationSumAggregateOutputType | null
|
||||
@@ -231,11 +231,11 @@ export type ProclamationWhereInput = {
|
||||
content?: Prisma.StringFilter<"Proclamation"> | string
|
||||
solfeggioFrequencyId?: Prisma.IntFilter<"Proclamation"> | number
|
||||
categoryId?: Prisma.IntFilter<"Proclamation"> | number
|
||||
subscriptionLevelId?: Prisma.IntFilter<"Proclamation"> | number
|
||||
groupId?: Prisma.IntFilter<"Proclamation"> | number
|
||||
solfeggioFrequency?: Prisma.XOR<Prisma.SolfeggioFrequencyScalarRelationFilter, Prisma.SolfeggioFrequencyWhereInput>
|
||||
category?: Prisma.XOR<Prisma.ProclamationCategoryScalarRelationFilter, Prisma.ProclamationCategoryWhereInput>
|
||||
group?: Prisma.XOR<Prisma.ProclamationGroupScalarRelationFilter, Prisma.ProclamationGroupWhereInput>
|
||||
userProclamations?: Prisma.UserProclamationsListRelationFilter
|
||||
subscriptionLevel?: Prisma.XOR<Prisma.SubscriptionLevelScalarRelationFilter, Prisma.SubscriptionLevelWhereInput>
|
||||
}
|
||||
|
||||
export type ProclamationOrderByWithRelationInput = {
|
||||
@@ -244,11 +244,11 @@ export type ProclamationOrderByWithRelationInput = {
|
||||
content?: Prisma.SortOrder
|
||||
solfeggioFrequencyId?: Prisma.SortOrder
|
||||
categoryId?: Prisma.SortOrder
|
||||
subscriptionLevelId?: Prisma.SortOrder
|
||||
groupId?: Prisma.SortOrder
|
||||
solfeggioFrequency?: Prisma.SolfeggioFrequencyOrderByWithRelationInput
|
||||
category?: Prisma.ProclamationCategoryOrderByWithRelationInput
|
||||
group?: Prisma.ProclamationGroupOrderByWithRelationInput
|
||||
userProclamations?: Prisma.UserProclamationsOrderByRelationAggregateInput
|
||||
subscriptionLevel?: Prisma.SubscriptionLevelOrderByWithRelationInput
|
||||
}
|
||||
|
||||
export type ProclamationWhereUniqueInput = Prisma.AtLeast<{
|
||||
@@ -260,11 +260,11 @@ export type ProclamationWhereUniqueInput = Prisma.AtLeast<{
|
||||
content?: Prisma.StringFilter<"Proclamation"> | string
|
||||
solfeggioFrequencyId?: Prisma.IntFilter<"Proclamation"> | number
|
||||
categoryId?: Prisma.IntFilter<"Proclamation"> | number
|
||||
subscriptionLevelId?: Prisma.IntFilter<"Proclamation"> | number
|
||||
groupId?: Prisma.IntFilter<"Proclamation"> | number
|
||||
solfeggioFrequency?: Prisma.XOR<Prisma.SolfeggioFrequencyScalarRelationFilter, Prisma.SolfeggioFrequencyWhereInput>
|
||||
category?: Prisma.XOR<Prisma.ProclamationCategoryScalarRelationFilter, Prisma.ProclamationCategoryWhereInput>
|
||||
group?: Prisma.XOR<Prisma.ProclamationGroupScalarRelationFilter, Prisma.ProclamationGroupWhereInput>
|
||||
userProclamations?: Prisma.UserProclamationsListRelationFilter
|
||||
subscriptionLevel?: Prisma.XOR<Prisma.SubscriptionLevelScalarRelationFilter, Prisma.SubscriptionLevelWhereInput>
|
||||
}, "id" | "name">
|
||||
|
||||
export type ProclamationOrderByWithAggregationInput = {
|
||||
@@ -273,7 +273,7 @@ export type ProclamationOrderByWithAggregationInput = {
|
||||
content?: Prisma.SortOrder
|
||||
solfeggioFrequencyId?: Prisma.SortOrder
|
||||
categoryId?: Prisma.SortOrder
|
||||
subscriptionLevelId?: Prisma.SortOrder
|
||||
groupId?: Prisma.SortOrder
|
||||
_count?: Prisma.ProclamationCountOrderByAggregateInput
|
||||
_avg?: Prisma.ProclamationAvgOrderByAggregateInput
|
||||
_max?: Prisma.ProclamationMaxOrderByAggregateInput
|
||||
@@ -290,7 +290,7 @@ export type ProclamationScalarWhereWithAggregatesInput = {
|
||||
content?: Prisma.StringWithAggregatesFilter<"Proclamation"> | string
|
||||
solfeggioFrequencyId?: Prisma.IntWithAggregatesFilter<"Proclamation"> | number
|
||||
categoryId?: Prisma.IntWithAggregatesFilter<"Proclamation"> | number
|
||||
subscriptionLevelId?: Prisma.IntWithAggregatesFilter<"Proclamation"> | number
|
||||
groupId?: Prisma.IntWithAggregatesFilter<"Proclamation"> | number
|
||||
}
|
||||
|
||||
export type ProclamationCreateInput = {
|
||||
@@ -299,8 +299,8 @@ export type ProclamationCreateInput = {
|
||||
content: string
|
||||
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
|
||||
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
|
||||
group: Prisma.ProclamationGroupCreateNestedOneWithoutProclamationsInput
|
||||
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
|
||||
subscriptionLevel: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput
|
||||
}
|
||||
|
||||
export type ProclamationUncheckedCreateInput = {
|
||||
@@ -309,7 +309,7 @@ export type ProclamationUncheckedCreateInput = {
|
||||
content: string
|
||||
solfeggioFrequencyId: number
|
||||
categoryId: number
|
||||
subscriptionLevelId: number
|
||||
groupId: number
|
||||
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
|
||||
}
|
||||
|
||||
@@ -319,8 +319,8 @@ export type ProclamationUpdateInput = {
|
||||
content?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
|
||||
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
|
||||
group?: Prisma.ProclamationGroupUpdateOneRequiredWithoutProclamationsNestedInput
|
||||
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
|
||||
subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput
|
||||
}
|
||||
|
||||
export type ProclamationUncheckedUpdateInput = {
|
||||
@@ -329,7 +329,7 @@ export type ProclamationUncheckedUpdateInput = {
|
||||
content?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
groupId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
|
||||
}
|
||||
|
||||
@@ -339,7 +339,7 @@ export type ProclamationCreateManyInput = {
|
||||
content: string
|
||||
solfeggioFrequencyId: number
|
||||
categoryId: number
|
||||
subscriptionLevelId: number
|
||||
groupId: number
|
||||
}
|
||||
|
||||
export type ProclamationUpdateManyMutationInput = {
|
||||
@@ -354,7 +354,7 @@ export type ProclamationUncheckedUpdateManyInput = {
|
||||
content?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
groupId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
}
|
||||
|
||||
export type ProclamationCountOrderByAggregateInput = {
|
||||
@@ -363,13 +363,13 @@ export type ProclamationCountOrderByAggregateInput = {
|
||||
content?: Prisma.SortOrder
|
||||
solfeggioFrequencyId?: Prisma.SortOrder
|
||||
categoryId?: Prisma.SortOrder
|
||||
subscriptionLevelId?: Prisma.SortOrder
|
||||
groupId?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type ProclamationAvgOrderByAggregateInput = {
|
||||
solfeggioFrequencyId?: Prisma.SortOrder
|
||||
categoryId?: Prisma.SortOrder
|
||||
subscriptionLevelId?: Prisma.SortOrder
|
||||
groupId?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type ProclamationMaxOrderByAggregateInput = {
|
||||
@@ -378,7 +378,7 @@ export type ProclamationMaxOrderByAggregateInput = {
|
||||
content?: Prisma.SortOrder
|
||||
solfeggioFrequencyId?: Prisma.SortOrder
|
||||
categoryId?: Prisma.SortOrder
|
||||
subscriptionLevelId?: Prisma.SortOrder
|
||||
groupId?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type ProclamationMinOrderByAggregateInput = {
|
||||
@@ -387,18 +387,13 @@ export type ProclamationMinOrderByAggregateInput = {
|
||||
content?: Prisma.SortOrder
|
||||
solfeggioFrequencyId?: Prisma.SortOrder
|
||||
categoryId?: Prisma.SortOrder
|
||||
subscriptionLevelId?: Prisma.SortOrder
|
||||
groupId?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type ProclamationSumOrderByAggregateInput = {
|
||||
solfeggioFrequencyId?: Prisma.SortOrder
|
||||
categoryId?: Prisma.SortOrder
|
||||
subscriptionLevelId?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type ProclamationScalarRelationFilter = {
|
||||
is?: Prisma.ProclamationWhereInput
|
||||
isNot?: Prisma.ProclamationWhereInput
|
||||
groupId?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type ProclamationListRelationFilter = {
|
||||
@@ -411,6 +406,53 @@ export type ProclamationOrderByRelationAggregateInput = {
|
||||
_count?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type ProclamationScalarRelationFilter = {
|
||||
is?: Prisma.ProclamationWhereInput
|
||||
isNot?: Prisma.ProclamationWhereInput
|
||||
}
|
||||
|
||||
export type ProclamationCreateNestedManyWithoutGroupInput = {
|
||||
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutGroupInput, Prisma.ProclamationUncheckedCreateWithoutGroupInput> | Prisma.ProclamationCreateWithoutGroupInput[] | Prisma.ProclamationUncheckedCreateWithoutGroupInput[]
|
||||
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutGroupInput | Prisma.ProclamationCreateOrConnectWithoutGroupInput[]
|
||||
createMany?: Prisma.ProclamationCreateManyGroupInputEnvelope
|
||||
connect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
|
||||
}
|
||||
|
||||
export type ProclamationUncheckedCreateNestedManyWithoutGroupInput = {
|
||||
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutGroupInput, Prisma.ProclamationUncheckedCreateWithoutGroupInput> | Prisma.ProclamationCreateWithoutGroupInput[] | Prisma.ProclamationUncheckedCreateWithoutGroupInput[]
|
||||
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutGroupInput | Prisma.ProclamationCreateOrConnectWithoutGroupInput[]
|
||||
createMany?: Prisma.ProclamationCreateManyGroupInputEnvelope
|
||||
connect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
|
||||
}
|
||||
|
||||
export type ProclamationUpdateManyWithoutGroupNestedInput = {
|
||||
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutGroupInput, Prisma.ProclamationUncheckedCreateWithoutGroupInput> | Prisma.ProclamationCreateWithoutGroupInput[] | Prisma.ProclamationUncheckedCreateWithoutGroupInput[]
|
||||
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutGroupInput | Prisma.ProclamationCreateOrConnectWithoutGroupInput[]
|
||||
upsert?: Prisma.ProclamationUpsertWithWhereUniqueWithoutGroupInput | Prisma.ProclamationUpsertWithWhereUniqueWithoutGroupInput[]
|
||||
createMany?: Prisma.ProclamationCreateManyGroupInputEnvelope
|
||||
set?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
|
||||
disconnect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
|
||||
delete?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
|
||||
connect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
|
||||
update?: Prisma.ProclamationUpdateWithWhereUniqueWithoutGroupInput | Prisma.ProclamationUpdateWithWhereUniqueWithoutGroupInput[]
|
||||
updateMany?: Prisma.ProclamationUpdateManyWithWhereWithoutGroupInput | Prisma.ProclamationUpdateManyWithWhereWithoutGroupInput[]
|
||||
deleteMany?: Prisma.ProclamationScalarWhereInput | Prisma.ProclamationScalarWhereInput[]
|
||||
}
|
||||
|
||||
export type ProclamationUncheckedUpdateManyWithoutGroupNestedInput = {
|
||||
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutGroupInput, Prisma.ProclamationUncheckedCreateWithoutGroupInput> | Prisma.ProclamationCreateWithoutGroupInput[] | Prisma.ProclamationUncheckedCreateWithoutGroupInput[]
|
||||
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutGroupInput | Prisma.ProclamationCreateOrConnectWithoutGroupInput[]
|
||||
upsert?: Prisma.ProclamationUpsertWithWhereUniqueWithoutGroupInput | Prisma.ProclamationUpsertWithWhereUniqueWithoutGroupInput[]
|
||||
createMany?: Prisma.ProclamationCreateManyGroupInputEnvelope
|
||||
set?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
|
||||
disconnect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
|
||||
delete?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
|
||||
connect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
|
||||
update?: Prisma.ProclamationUpdateWithWhereUniqueWithoutGroupInput | Prisma.ProclamationUpdateWithWhereUniqueWithoutGroupInput[]
|
||||
updateMany?: Prisma.ProclamationUpdateManyWithWhereWithoutGroupInput | Prisma.ProclamationUpdateManyWithWhereWithoutGroupInput[]
|
||||
deleteMany?: Prisma.ProclamationScalarWhereInput | Prisma.ProclamationScalarWhereInput[]
|
||||
}
|
||||
|
||||
export type ProclamationCreateNestedOneWithoutUserProclamationsInput = {
|
||||
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutUserProclamationsInput, Prisma.ProclamationUncheckedCreateWithoutUserProclamationsInput>
|
||||
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutUserProclamationsInput
|
||||
@@ -509,46 +551,60 @@ export type ProclamationUncheckedUpdateManyWithoutCategoryNestedInput = {
|
||||
deleteMany?: Prisma.ProclamationScalarWhereInput | Prisma.ProclamationScalarWhereInput[]
|
||||
}
|
||||
|
||||
export type ProclamationCreateNestedManyWithoutSubscriptionLevelInput = {
|
||||
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput> | Prisma.ProclamationCreateWithoutSubscriptionLevelInput[] | Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput[]
|
||||
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutSubscriptionLevelInput | Prisma.ProclamationCreateOrConnectWithoutSubscriptionLevelInput[]
|
||||
createMany?: Prisma.ProclamationCreateManySubscriptionLevelInputEnvelope
|
||||
connect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
|
||||
export type ProclamationCreateWithoutGroupInput = {
|
||||
id?: string
|
||||
name: string
|
||||
content: string
|
||||
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
|
||||
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
|
||||
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
|
||||
}
|
||||
|
||||
export type ProclamationUncheckedCreateNestedManyWithoutSubscriptionLevelInput = {
|
||||
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput> | Prisma.ProclamationCreateWithoutSubscriptionLevelInput[] | Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput[]
|
||||
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutSubscriptionLevelInput | Prisma.ProclamationCreateOrConnectWithoutSubscriptionLevelInput[]
|
||||
createMany?: Prisma.ProclamationCreateManySubscriptionLevelInputEnvelope
|
||||
connect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
|
||||
export type ProclamationUncheckedCreateWithoutGroupInput = {
|
||||
id?: string
|
||||
name: string
|
||||
content: string
|
||||
solfeggioFrequencyId: number
|
||||
categoryId: number
|
||||
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
|
||||
}
|
||||
|
||||
export type ProclamationUpdateManyWithoutSubscriptionLevelNestedInput = {
|
||||
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput> | Prisma.ProclamationCreateWithoutSubscriptionLevelInput[] | Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput[]
|
||||
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutSubscriptionLevelInput | Prisma.ProclamationCreateOrConnectWithoutSubscriptionLevelInput[]
|
||||
upsert?: Prisma.ProclamationUpsertWithWhereUniqueWithoutSubscriptionLevelInput | Prisma.ProclamationUpsertWithWhereUniqueWithoutSubscriptionLevelInput[]
|
||||
createMany?: Prisma.ProclamationCreateManySubscriptionLevelInputEnvelope
|
||||
set?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
|
||||
disconnect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
|
||||
delete?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
|
||||
connect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
|
||||
update?: Prisma.ProclamationUpdateWithWhereUniqueWithoutSubscriptionLevelInput | Prisma.ProclamationUpdateWithWhereUniqueWithoutSubscriptionLevelInput[]
|
||||
updateMany?: Prisma.ProclamationUpdateManyWithWhereWithoutSubscriptionLevelInput | Prisma.ProclamationUpdateManyWithWhereWithoutSubscriptionLevelInput[]
|
||||
deleteMany?: Prisma.ProclamationScalarWhereInput | Prisma.ProclamationScalarWhereInput[]
|
||||
export type ProclamationCreateOrConnectWithoutGroupInput = {
|
||||
where: Prisma.ProclamationWhereUniqueInput
|
||||
create: Prisma.XOR<Prisma.ProclamationCreateWithoutGroupInput, Prisma.ProclamationUncheckedCreateWithoutGroupInput>
|
||||
}
|
||||
|
||||
export type ProclamationUncheckedUpdateManyWithoutSubscriptionLevelNestedInput = {
|
||||
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput> | Prisma.ProclamationCreateWithoutSubscriptionLevelInput[] | Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput[]
|
||||
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutSubscriptionLevelInput | Prisma.ProclamationCreateOrConnectWithoutSubscriptionLevelInput[]
|
||||
upsert?: Prisma.ProclamationUpsertWithWhereUniqueWithoutSubscriptionLevelInput | Prisma.ProclamationUpsertWithWhereUniqueWithoutSubscriptionLevelInput[]
|
||||
createMany?: Prisma.ProclamationCreateManySubscriptionLevelInputEnvelope
|
||||
set?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
|
||||
disconnect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
|
||||
delete?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
|
||||
connect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
|
||||
update?: Prisma.ProclamationUpdateWithWhereUniqueWithoutSubscriptionLevelInput | Prisma.ProclamationUpdateWithWhereUniqueWithoutSubscriptionLevelInput[]
|
||||
updateMany?: Prisma.ProclamationUpdateManyWithWhereWithoutSubscriptionLevelInput | Prisma.ProclamationUpdateManyWithWhereWithoutSubscriptionLevelInput[]
|
||||
deleteMany?: Prisma.ProclamationScalarWhereInput | Prisma.ProclamationScalarWhereInput[]
|
||||
export type ProclamationCreateManyGroupInputEnvelope = {
|
||||
data: Prisma.ProclamationCreateManyGroupInput | Prisma.ProclamationCreateManyGroupInput[]
|
||||
skipDuplicates?: boolean
|
||||
}
|
||||
|
||||
export type ProclamationUpsertWithWhereUniqueWithoutGroupInput = {
|
||||
where: Prisma.ProclamationWhereUniqueInput
|
||||
update: Prisma.XOR<Prisma.ProclamationUpdateWithoutGroupInput, Prisma.ProclamationUncheckedUpdateWithoutGroupInput>
|
||||
create: Prisma.XOR<Prisma.ProclamationCreateWithoutGroupInput, Prisma.ProclamationUncheckedCreateWithoutGroupInput>
|
||||
}
|
||||
|
||||
export type ProclamationUpdateWithWhereUniqueWithoutGroupInput = {
|
||||
where: Prisma.ProclamationWhereUniqueInput
|
||||
data: Prisma.XOR<Prisma.ProclamationUpdateWithoutGroupInput, Prisma.ProclamationUncheckedUpdateWithoutGroupInput>
|
||||
}
|
||||
|
||||
export type ProclamationUpdateManyWithWhereWithoutGroupInput = {
|
||||
where: Prisma.ProclamationScalarWhereInput
|
||||
data: Prisma.XOR<Prisma.ProclamationUpdateManyMutationInput, Prisma.ProclamationUncheckedUpdateManyWithoutGroupInput>
|
||||
}
|
||||
|
||||
export type ProclamationScalarWhereInput = {
|
||||
AND?: Prisma.ProclamationScalarWhereInput | Prisma.ProclamationScalarWhereInput[]
|
||||
OR?: Prisma.ProclamationScalarWhereInput[]
|
||||
NOT?: Prisma.ProclamationScalarWhereInput | Prisma.ProclamationScalarWhereInput[]
|
||||
id?: Prisma.StringFilter<"Proclamation"> | string
|
||||
name?: Prisma.StringFilter<"Proclamation"> | string
|
||||
content?: Prisma.StringFilter<"Proclamation"> | string
|
||||
solfeggioFrequencyId?: Prisma.IntFilter<"Proclamation"> | number
|
||||
categoryId?: Prisma.IntFilter<"Proclamation"> | number
|
||||
groupId?: Prisma.IntFilter<"Proclamation"> | number
|
||||
}
|
||||
|
||||
export type ProclamationCreateWithoutUserProclamationsInput = {
|
||||
@@ -557,7 +613,7 @@ export type ProclamationCreateWithoutUserProclamationsInput = {
|
||||
content: string
|
||||
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
|
||||
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
|
||||
subscriptionLevel: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput
|
||||
group: Prisma.ProclamationGroupCreateNestedOneWithoutProclamationsInput
|
||||
}
|
||||
|
||||
export type ProclamationUncheckedCreateWithoutUserProclamationsInput = {
|
||||
@@ -566,7 +622,7 @@ export type ProclamationUncheckedCreateWithoutUserProclamationsInput = {
|
||||
content: string
|
||||
solfeggioFrequencyId: number
|
||||
categoryId: number
|
||||
subscriptionLevelId: number
|
||||
groupId: number
|
||||
}
|
||||
|
||||
export type ProclamationCreateOrConnectWithoutUserProclamationsInput = {
|
||||
@@ -591,7 +647,7 @@ export type ProclamationUpdateWithoutUserProclamationsInput = {
|
||||
content?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
|
||||
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
|
||||
subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput
|
||||
group?: Prisma.ProclamationGroupUpdateOneRequiredWithoutProclamationsNestedInput
|
||||
}
|
||||
|
||||
export type ProclamationUncheckedUpdateWithoutUserProclamationsInput = {
|
||||
@@ -600,7 +656,7 @@ export type ProclamationUncheckedUpdateWithoutUserProclamationsInput = {
|
||||
content?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
groupId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
}
|
||||
|
||||
export type ProclamationCreateWithoutSolfeggioFrequencyInput = {
|
||||
@@ -608,8 +664,8 @@ export type ProclamationCreateWithoutSolfeggioFrequencyInput = {
|
||||
name: string
|
||||
content: string
|
||||
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
|
||||
group: Prisma.ProclamationGroupCreateNestedOneWithoutProclamationsInput
|
||||
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
|
||||
subscriptionLevel: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput
|
||||
}
|
||||
|
||||
export type ProclamationUncheckedCreateWithoutSolfeggioFrequencyInput = {
|
||||
@@ -617,7 +673,7 @@ export type ProclamationUncheckedCreateWithoutSolfeggioFrequencyInput = {
|
||||
name: string
|
||||
content: string
|
||||
categoryId: number
|
||||
subscriptionLevelId: number
|
||||
groupId: number
|
||||
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
|
||||
}
|
||||
|
||||
@@ -647,25 +703,13 @@ export type ProclamationUpdateManyWithWhereWithoutSolfeggioFrequencyInput = {
|
||||
data: Prisma.XOR<Prisma.ProclamationUpdateManyMutationInput, Prisma.ProclamationUncheckedUpdateManyWithoutSolfeggioFrequencyInput>
|
||||
}
|
||||
|
||||
export type ProclamationScalarWhereInput = {
|
||||
AND?: Prisma.ProclamationScalarWhereInput | Prisma.ProclamationScalarWhereInput[]
|
||||
OR?: Prisma.ProclamationScalarWhereInput[]
|
||||
NOT?: Prisma.ProclamationScalarWhereInput | Prisma.ProclamationScalarWhereInput[]
|
||||
id?: Prisma.StringFilter<"Proclamation"> | string
|
||||
name?: Prisma.StringFilter<"Proclamation"> | string
|
||||
content?: Prisma.StringFilter<"Proclamation"> | string
|
||||
solfeggioFrequencyId?: Prisma.IntFilter<"Proclamation"> | number
|
||||
categoryId?: Prisma.IntFilter<"Proclamation"> | number
|
||||
subscriptionLevelId?: Prisma.IntFilter<"Proclamation"> | number
|
||||
}
|
||||
|
||||
export type ProclamationCreateWithoutCategoryInput = {
|
||||
id?: string
|
||||
name: string
|
||||
content: string
|
||||
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
|
||||
group: Prisma.ProclamationGroupCreateNestedOneWithoutProclamationsInput
|
||||
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
|
||||
subscriptionLevel: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput
|
||||
}
|
||||
|
||||
export type ProclamationUncheckedCreateWithoutCategoryInput = {
|
||||
@@ -673,7 +717,7 @@ export type ProclamationUncheckedCreateWithoutCategoryInput = {
|
||||
name: string
|
||||
content: string
|
||||
solfeggioFrequencyId: number
|
||||
subscriptionLevelId: number
|
||||
groupId: number
|
||||
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
|
||||
}
|
||||
|
||||
@@ -703,48 +747,38 @@ export type ProclamationUpdateManyWithWhereWithoutCategoryInput = {
|
||||
data: Prisma.XOR<Prisma.ProclamationUpdateManyMutationInput, Prisma.ProclamationUncheckedUpdateManyWithoutCategoryInput>
|
||||
}
|
||||
|
||||
export type ProclamationCreateWithoutSubscriptionLevelInput = {
|
||||
id?: string
|
||||
name: string
|
||||
content: string
|
||||
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
|
||||
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
|
||||
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
|
||||
}
|
||||
|
||||
export type ProclamationUncheckedCreateWithoutSubscriptionLevelInput = {
|
||||
export type ProclamationCreateManyGroupInput = {
|
||||
id?: string
|
||||
name: string
|
||||
content: string
|
||||
solfeggioFrequencyId: number
|
||||
categoryId: number
|
||||
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
|
||||
}
|
||||
|
||||
export type ProclamationCreateOrConnectWithoutSubscriptionLevelInput = {
|
||||
where: Prisma.ProclamationWhereUniqueInput
|
||||
create: Prisma.XOR<Prisma.ProclamationCreateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput>
|
||||
export type ProclamationUpdateWithoutGroupInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
content?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
|
||||
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
|
||||
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
|
||||
}
|
||||
|
||||
export type ProclamationCreateManySubscriptionLevelInputEnvelope = {
|
||||
data: Prisma.ProclamationCreateManySubscriptionLevelInput | Prisma.ProclamationCreateManySubscriptionLevelInput[]
|
||||
skipDuplicates?: boolean
|
||||
export type ProclamationUncheckedUpdateWithoutGroupInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
content?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
|
||||
}
|
||||
|
||||
export type ProclamationUpsertWithWhereUniqueWithoutSubscriptionLevelInput = {
|
||||
where: Prisma.ProclamationWhereUniqueInput
|
||||
update: Prisma.XOR<Prisma.ProclamationUpdateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedUpdateWithoutSubscriptionLevelInput>
|
||||
create: Prisma.XOR<Prisma.ProclamationCreateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput>
|
||||
}
|
||||
|
||||
export type ProclamationUpdateWithWhereUniqueWithoutSubscriptionLevelInput = {
|
||||
where: Prisma.ProclamationWhereUniqueInput
|
||||
data: Prisma.XOR<Prisma.ProclamationUpdateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedUpdateWithoutSubscriptionLevelInput>
|
||||
}
|
||||
|
||||
export type ProclamationUpdateManyWithWhereWithoutSubscriptionLevelInput = {
|
||||
where: Prisma.ProclamationScalarWhereInput
|
||||
data: Prisma.XOR<Prisma.ProclamationUpdateManyMutationInput, Prisma.ProclamationUncheckedUpdateManyWithoutSubscriptionLevelInput>
|
||||
export type ProclamationUncheckedUpdateManyWithoutGroupInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
content?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
}
|
||||
|
||||
export type ProclamationCreateManySolfeggioFrequencyInput = {
|
||||
@@ -752,7 +786,7 @@ export type ProclamationCreateManySolfeggioFrequencyInput = {
|
||||
name: string
|
||||
content: string
|
||||
categoryId: number
|
||||
subscriptionLevelId: number
|
||||
groupId: number
|
||||
}
|
||||
|
||||
export type ProclamationUpdateWithoutSolfeggioFrequencyInput = {
|
||||
@@ -760,8 +794,8 @@ export type ProclamationUpdateWithoutSolfeggioFrequencyInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
content?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
|
||||
group?: Prisma.ProclamationGroupUpdateOneRequiredWithoutProclamationsNestedInput
|
||||
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
|
||||
subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput
|
||||
}
|
||||
|
||||
export type ProclamationUncheckedUpdateWithoutSolfeggioFrequencyInput = {
|
||||
@@ -769,7 +803,7 @@ export type ProclamationUncheckedUpdateWithoutSolfeggioFrequencyInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
content?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
groupId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
|
||||
}
|
||||
|
||||
@@ -778,7 +812,7 @@ export type ProclamationUncheckedUpdateManyWithoutSolfeggioFrequencyInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
content?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
groupId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
}
|
||||
|
||||
export type ProclamationCreateManyCategoryInput = {
|
||||
@@ -786,7 +820,7 @@ export type ProclamationCreateManyCategoryInput = {
|
||||
name: string
|
||||
content: string
|
||||
solfeggioFrequencyId: number
|
||||
subscriptionLevelId: number
|
||||
groupId: number
|
||||
}
|
||||
|
||||
export type ProclamationUpdateWithoutCategoryInput = {
|
||||
@@ -794,8 +828,8 @@ export type ProclamationUpdateWithoutCategoryInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
content?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
|
||||
group?: Prisma.ProclamationGroupUpdateOneRequiredWithoutProclamationsNestedInput
|
||||
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
|
||||
subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput
|
||||
}
|
||||
|
||||
export type ProclamationUncheckedUpdateWithoutCategoryInput = {
|
||||
@@ -803,7 +837,7 @@ export type ProclamationUncheckedUpdateWithoutCategoryInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
content?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
groupId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
|
||||
}
|
||||
|
||||
@@ -812,41 +846,7 @@ export type ProclamationUncheckedUpdateManyWithoutCategoryInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
content?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
}
|
||||
|
||||
export type ProclamationCreateManySubscriptionLevelInput = {
|
||||
id?: string
|
||||
name: string
|
||||
content: string
|
||||
solfeggioFrequencyId: number
|
||||
categoryId: number
|
||||
}
|
||||
|
||||
export type ProclamationUpdateWithoutSubscriptionLevelInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
content?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
|
||||
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
|
||||
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
|
||||
}
|
||||
|
||||
export type ProclamationUncheckedUpdateWithoutSubscriptionLevelInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
content?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
|
||||
}
|
||||
|
||||
export type ProclamationUncheckedUpdateManyWithoutSubscriptionLevelInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
content?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
groupId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
}
|
||||
|
||||
|
||||
@@ -886,11 +886,11 @@ export type ProclamationSelect<ExtArgs extends runtime.Types.Extensions.Internal
|
||||
content?: boolean
|
||||
solfeggioFrequencyId?: boolean
|
||||
categoryId?: boolean
|
||||
subscriptionLevelId?: boolean
|
||||
groupId?: boolean
|
||||
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
|
||||
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
|
||||
group?: boolean | Prisma.ProclamationGroupDefaultArgs<ExtArgs>
|
||||
userProclamations?: boolean | Prisma.Proclamation$userProclamationsArgs<ExtArgs>
|
||||
subscriptionLevel?: boolean | Prisma.SubscriptionLevelDefaultArgs<ExtArgs>
|
||||
_count?: boolean | Prisma.ProclamationCountOutputTypeDefaultArgs<ExtArgs>
|
||||
}, ExtArgs["result"]["proclamation"]>
|
||||
|
||||
@@ -900,10 +900,10 @@ export type ProclamationSelectCreateManyAndReturn<ExtArgs extends runtime.Types.
|
||||
content?: boolean
|
||||
solfeggioFrequencyId?: boolean
|
||||
categoryId?: boolean
|
||||
subscriptionLevelId?: boolean
|
||||
groupId?: boolean
|
||||
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
|
||||
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
|
||||
subscriptionLevel?: boolean | Prisma.SubscriptionLevelDefaultArgs<ExtArgs>
|
||||
group?: boolean | Prisma.ProclamationGroupDefaultArgs<ExtArgs>
|
||||
}, ExtArgs["result"]["proclamation"]>
|
||||
|
||||
export type ProclamationSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||
@@ -912,10 +912,10 @@ export type ProclamationSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.
|
||||
content?: boolean
|
||||
solfeggioFrequencyId?: boolean
|
||||
categoryId?: boolean
|
||||
subscriptionLevelId?: boolean
|
||||
groupId?: boolean
|
||||
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
|
||||
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
|
||||
subscriptionLevel?: boolean | Prisma.SubscriptionLevelDefaultArgs<ExtArgs>
|
||||
group?: boolean | Prisma.ProclamationGroupDefaultArgs<ExtArgs>
|
||||
}, ExtArgs["result"]["proclamation"]>
|
||||
|
||||
export type ProclamationSelectScalar = {
|
||||
@@ -924,26 +924,26 @@ export type ProclamationSelectScalar = {
|
||||
content?: boolean
|
||||
solfeggioFrequencyId?: boolean
|
||||
categoryId?: boolean
|
||||
subscriptionLevelId?: boolean
|
||||
groupId?: boolean
|
||||
}
|
||||
|
||||
export type ProclamationOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "content" | "solfeggioFrequencyId" | "categoryId" | "subscriptionLevelId", ExtArgs["result"]["proclamation"]>
|
||||
export type ProclamationOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "content" | "solfeggioFrequencyId" | "categoryId" | "groupId", ExtArgs["result"]["proclamation"]>
|
||||
export type ProclamationInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
|
||||
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
|
||||
group?: boolean | Prisma.ProclamationGroupDefaultArgs<ExtArgs>
|
||||
userProclamations?: boolean | Prisma.Proclamation$userProclamationsArgs<ExtArgs>
|
||||
subscriptionLevel?: boolean | Prisma.SubscriptionLevelDefaultArgs<ExtArgs>
|
||||
_count?: boolean | Prisma.ProclamationCountOutputTypeDefaultArgs<ExtArgs>
|
||||
}
|
||||
export type ProclamationIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
|
||||
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
|
||||
subscriptionLevel?: boolean | Prisma.SubscriptionLevelDefaultArgs<ExtArgs>
|
||||
group?: boolean | Prisma.ProclamationGroupDefaultArgs<ExtArgs>
|
||||
}
|
||||
export type ProclamationIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
|
||||
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
|
||||
subscriptionLevel?: boolean | Prisma.SubscriptionLevelDefaultArgs<ExtArgs>
|
||||
group?: boolean | Prisma.ProclamationGroupDefaultArgs<ExtArgs>
|
||||
}
|
||||
|
||||
export type $ProclamationPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
@@ -951,8 +951,8 @@ export type $ProclamationPayload<ExtArgs extends runtime.Types.Extensions.Intern
|
||||
objects: {
|
||||
solfeggioFrequency: Prisma.$SolfeggioFrequencyPayload<ExtArgs>
|
||||
category: Prisma.$ProclamationCategoryPayload<ExtArgs>
|
||||
group: Prisma.$ProclamationGroupPayload<ExtArgs>
|
||||
userProclamations: Prisma.$UserProclamationsPayload<ExtArgs>[]
|
||||
subscriptionLevel: Prisma.$SubscriptionLevelPayload<ExtArgs>
|
||||
}
|
||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||
id: string
|
||||
@@ -960,7 +960,7 @@ export type $ProclamationPayload<ExtArgs extends runtime.Types.Extensions.Intern
|
||||
content: string
|
||||
solfeggioFrequencyId: number
|
||||
categoryId: number
|
||||
subscriptionLevelId: number
|
||||
groupId: number
|
||||
}, ExtArgs["result"]["proclamation"]>
|
||||
composites: {}
|
||||
}
|
||||
@@ -1357,8 +1357,8 @@ export interface Prisma__ProclamationClient<T, Null = never, ExtArgs extends run
|
||||
readonly [Symbol.toStringTag]: "PrismaPromise"
|
||||
solfeggioFrequency<T extends Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>>): Prisma.Prisma__SolfeggioFrequencyClient<runtime.Types.Result.GetResult<Prisma.$SolfeggioFrequencyPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
|
||||
category<T extends Prisma.ProclamationCategoryDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.ProclamationCategoryDefaultArgs<ExtArgs>>): Prisma.Prisma__ProclamationCategoryClient<runtime.Types.Result.GetResult<Prisma.$ProclamationCategoryPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
|
||||
group<T extends Prisma.ProclamationGroupDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.ProclamationGroupDefaultArgs<ExtArgs>>): Prisma.Prisma__ProclamationGroupClient<runtime.Types.Result.GetResult<Prisma.$ProclamationGroupPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
|
||||
userProclamations<T extends Prisma.Proclamation$userProclamationsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Proclamation$userProclamationsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$UserProclamationsPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
subscriptionLevel<T extends Prisma.SubscriptionLevelDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.SubscriptionLevelDefaultArgs<ExtArgs>>): Prisma.Prisma__SubscriptionLevelClient<runtime.Types.Result.GetResult<Prisma.$SubscriptionLevelPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
|
||||
/**
|
||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
||||
@@ -1393,7 +1393,7 @@ export interface ProclamationFieldRefs {
|
||||
readonly content: Prisma.FieldRef<"Proclamation", 'String'>
|
||||
readonly solfeggioFrequencyId: Prisma.FieldRef<"Proclamation", 'Int'>
|
||||
readonly categoryId: Prisma.FieldRef<"Proclamation", 'Int'>
|
||||
readonly subscriptionLevelId: Prisma.FieldRef<"Proclamation", 'Int'>
|
||||
readonly groupId: Prisma.FieldRef<"Proclamation", 'Int'>
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -29,34 +29,29 @@ export type AggregateProclamationCategory = {
|
||||
export type ProclamationCategoryAvgAggregateOutputType = {
|
||||
id: number | null
|
||||
sortOrder: number | null
|
||||
parentId: number | null
|
||||
}
|
||||
|
||||
export type ProclamationCategorySumAggregateOutputType = {
|
||||
id: number | null
|
||||
sortOrder: number | null
|
||||
parentId: number | null
|
||||
}
|
||||
|
||||
export type ProclamationCategoryMinAggregateOutputType = {
|
||||
id: number | null
|
||||
name: string | null
|
||||
sortOrder: number | null
|
||||
parentId: number | null
|
||||
}
|
||||
|
||||
export type ProclamationCategoryMaxAggregateOutputType = {
|
||||
id: number | null
|
||||
name: string | null
|
||||
sortOrder: number | null
|
||||
parentId: number | null
|
||||
}
|
||||
|
||||
export type ProclamationCategoryCountAggregateOutputType = {
|
||||
id: number
|
||||
name: number
|
||||
sortOrder: number
|
||||
parentId: number
|
||||
_all: number
|
||||
}
|
||||
|
||||
@@ -64,34 +59,29 @@ export type ProclamationCategoryCountAggregateOutputType = {
|
||||
export type ProclamationCategoryAvgAggregateInputType = {
|
||||
id?: true
|
||||
sortOrder?: true
|
||||
parentId?: true
|
||||
}
|
||||
|
||||
export type ProclamationCategorySumAggregateInputType = {
|
||||
id?: true
|
||||
sortOrder?: true
|
||||
parentId?: true
|
||||
}
|
||||
|
||||
export type ProclamationCategoryMinAggregateInputType = {
|
||||
id?: true
|
||||
name?: true
|
||||
sortOrder?: true
|
||||
parentId?: true
|
||||
}
|
||||
|
||||
export type ProclamationCategoryMaxAggregateInputType = {
|
||||
id?: true
|
||||
name?: true
|
||||
sortOrder?: true
|
||||
parentId?: true
|
||||
}
|
||||
|
||||
export type ProclamationCategoryCountAggregateInputType = {
|
||||
id?: true
|
||||
name?: true
|
||||
sortOrder?: true
|
||||
parentId?: true
|
||||
_all?: true
|
||||
}
|
||||
|
||||
@@ -185,7 +175,6 @@ export type ProclamationCategoryGroupByOutputType = {
|
||||
id: number
|
||||
name: string
|
||||
sortOrder: number
|
||||
parentId: number | null
|
||||
_count: ProclamationCategoryCountAggregateOutputType | null
|
||||
_avg: ProclamationCategoryAvgAggregateOutputType | null
|
||||
_sum: ProclamationCategorySumAggregateOutputType | null
|
||||
@@ -215,44 +204,30 @@ export type ProclamationCategoryWhereInput = {
|
||||
id?: Prisma.IntFilter<"ProclamationCategory"> | number
|
||||
name?: Prisma.StringFilter<"ProclamationCategory"> | string
|
||||
sortOrder?: Prisma.IntFilter<"ProclamationCategory"> | number
|
||||
parentId?: Prisma.IntNullableFilter<"ProclamationCategory"> | number | null
|
||||
parent?: Prisma.XOR<Prisma.ProclamationCategoryNullableScalarRelationFilter, Prisma.ProclamationCategoryWhereInput> | null
|
||||
children?: Prisma.ProclamationCategoryListRelationFilter
|
||||
proclamations?: Prisma.ProclamationListRelationFilter
|
||||
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelListRelationFilter
|
||||
}
|
||||
|
||||
export type ProclamationCategoryOrderByWithRelationInput = {
|
||||
id?: Prisma.SortOrder
|
||||
name?: Prisma.SortOrder
|
||||
sortOrder?: Prisma.SortOrder
|
||||
parentId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
parent?: Prisma.ProclamationCategoryOrderByWithRelationInput
|
||||
children?: Prisma.ProclamationCategoryOrderByRelationAggregateInput
|
||||
proclamations?: Prisma.ProclamationOrderByRelationAggregateInput
|
||||
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelOrderByRelationAggregateInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryWhereUniqueInput = Prisma.AtLeast<{
|
||||
id?: number
|
||||
parentId_name?: Prisma.ProclamationCategoryParentIdNameCompoundUniqueInput
|
||||
name?: string
|
||||
AND?: Prisma.ProclamationCategoryWhereInput | Prisma.ProclamationCategoryWhereInput[]
|
||||
OR?: Prisma.ProclamationCategoryWhereInput[]
|
||||
NOT?: Prisma.ProclamationCategoryWhereInput | Prisma.ProclamationCategoryWhereInput[]
|
||||
name?: Prisma.StringFilter<"ProclamationCategory"> | string
|
||||
sortOrder?: Prisma.IntFilter<"ProclamationCategory"> | number
|
||||
parentId?: Prisma.IntNullableFilter<"ProclamationCategory"> | number | null
|
||||
parent?: Prisma.XOR<Prisma.ProclamationCategoryNullableScalarRelationFilter, Prisma.ProclamationCategoryWhereInput> | null
|
||||
children?: Prisma.ProclamationCategoryListRelationFilter
|
||||
proclamations?: Prisma.ProclamationListRelationFilter
|
||||
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelListRelationFilter
|
||||
}, "id" | "parentId_name">
|
||||
}, "id" | "name">
|
||||
|
||||
export type ProclamationCategoryOrderByWithAggregationInput = {
|
||||
id?: Prisma.SortOrder
|
||||
name?: Prisma.SortOrder
|
||||
sortOrder?: Prisma.SortOrder
|
||||
parentId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
_count?: Prisma.ProclamationCategoryCountOrderByAggregateInput
|
||||
_avg?: Prisma.ProclamationCategoryAvgOrderByAggregateInput
|
||||
_max?: Prisma.ProclamationCategoryMaxOrderByAggregateInput
|
||||
@@ -267,52 +242,38 @@ export type ProclamationCategoryScalarWhereWithAggregatesInput = {
|
||||
id?: Prisma.IntWithAggregatesFilter<"ProclamationCategory"> | number
|
||||
name?: Prisma.StringWithAggregatesFilter<"ProclamationCategory"> | string
|
||||
sortOrder?: Prisma.IntWithAggregatesFilter<"ProclamationCategory"> | number
|
||||
parentId?: Prisma.IntNullableWithAggregatesFilter<"ProclamationCategory"> | number | null
|
||||
}
|
||||
|
||||
export type ProclamationCategoryCreateInput = {
|
||||
name: string
|
||||
sortOrder?: number
|
||||
parent?: Prisma.ProclamationCategoryCreateNestedOneWithoutChildrenInput
|
||||
children?: Prisma.ProclamationCategoryCreateNestedManyWithoutParentInput
|
||||
proclamations?: Prisma.ProclamationCreateNestedManyWithoutCategoryInput
|
||||
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelCreateNestedManyWithoutCategoryInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUncheckedCreateInput = {
|
||||
id?: number
|
||||
name: string
|
||||
sortOrder?: number
|
||||
parentId?: number | null
|
||||
children?: Prisma.ProclamationCategoryUncheckedCreateNestedManyWithoutParentInput
|
||||
proclamations?: Prisma.ProclamationUncheckedCreateNestedManyWithoutCategoryInput
|
||||
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUncheckedCreateNestedManyWithoutCategoryInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUpdateInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
parent?: Prisma.ProclamationCategoryUpdateOneWithoutChildrenNestedInput
|
||||
children?: Prisma.ProclamationCategoryUpdateManyWithoutParentNestedInput
|
||||
proclamations?: Prisma.ProclamationUpdateManyWithoutCategoryNestedInput
|
||||
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUpdateManyWithoutCategoryNestedInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUncheckedUpdateInput = {
|
||||
id?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
parentId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
children?: Prisma.ProclamationCategoryUncheckedUpdateManyWithoutParentNestedInput
|
||||
proclamations?: Prisma.ProclamationUncheckedUpdateManyWithoutCategoryNestedInput
|
||||
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUncheckedUpdateManyWithoutCategoryNestedInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryCreateManyInput = {
|
||||
id?: number
|
||||
name: string
|
||||
sortOrder?: number
|
||||
parentId?: number | null
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUpdateManyMutationInput = {
|
||||
@@ -324,7 +285,6 @@ export type ProclamationCategoryUncheckedUpdateManyInput = {
|
||||
id?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
parentId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
}
|
||||
|
||||
export type ProclamationCategoryScalarRelationFilter = {
|
||||
@@ -332,57 +292,32 @@ export type ProclamationCategoryScalarRelationFilter = {
|
||||
isNot?: Prisma.ProclamationCategoryWhereInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryNullableScalarRelationFilter = {
|
||||
is?: Prisma.ProclamationCategoryWhereInput | null
|
||||
isNot?: Prisma.ProclamationCategoryWhereInput | null
|
||||
}
|
||||
|
||||
export type ProclamationCategoryListRelationFilter = {
|
||||
every?: Prisma.ProclamationCategoryWhereInput
|
||||
some?: Prisma.ProclamationCategoryWhereInput
|
||||
none?: Prisma.ProclamationCategoryWhereInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryOrderByRelationAggregateInput = {
|
||||
_count?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type ProclamationCategoryParentIdNameCompoundUniqueInput = {
|
||||
parentId: number
|
||||
name: string
|
||||
}
|
||||
|
||||
export type ProclamationCategoryCountOrderByAggregateInput = {
|
||||
id?: Prisma.SortOrder
|
||||
name?: Prisma.SortOrder
|
||||
sortOrder?: Prisma.SortOrder
|
||||
parentId?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type ProclamationCategoryAvgOrderByAggregateInput = {
|
||||
id?: Prisma.SortOrder
|
||||
sortOrder?: Prisma.SortOrder
|
||||
parentId?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type ProclamationCategoryMaxOrderByAggregateInput = {
|
||||
id?: Prisma.SortOrder
|
||||
name?: Prisma.SortOrder
|
||||
sortOrder?: Prisma.SortOrder
|
||||
parentId?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type ProclamationCategoryMinOrderByAggregateInput = {
|
||||
id?: Prisma.SortOrder
|
||||
name?: Prisma.SortOrder
|
||||
sortOrder?: Prisma.SortOrder
|
||||
parentId?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type ProclamationCategorySumOrderByAggregateInput = {
|
||||
id?: Prisma.SortOrder
|
||||
sortOrder?: Prisma.SortOrder
|
||||
parentId?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type ProclamationCategoryCreateNestedOneWithoutProclamationsInput = {
|
||||
@@ -399,93 +334,15 @@ export type ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
|
||||
update?: Prisma.XOR<Prisma.XOR<Prisma.ProclamationCategoryUpdateToOneWithWhereWithoutProclamationsInput, Prisma.ProclamationCategoryUpdateWithoutProclamationsInput>, Prisma.ProclamationCategoryUncheckedUpdateWithoutProclamationsInput>
|
||||
}
|
||||
|
||||
export type ProclamationCategoryCreateNestedOneWithoutChildrenInput = {
|
||||
create?: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutChildrenInput, Prisma.ProclamationCategoryUncheckedCreateWithoutChildrenInput>
|
||||
connectOrCreate?: Prisma.ProclamationCategoryCreateOrConnectWithoutChildrenInput
|
||||
connect?: Prisma.ProclamationCategoryWhereUniqueInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryCreateNestedManyWithoutParentInput = {
|
||||
create?: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutParentInput, Prisma.ProclamationCategoryUncheckedCreateWithoutParentInput> | Prisma.ProclamationCategoryCreateWithoutParentInput[] | Prisma.ProclamationCategoryUncheckedCreateWithoutParentInput[]
|
||||
connectOrCreate?: Prisma.ProclamationCategoryCreateOrConnectWithoutParentInput | Prisma.ProclamationCategoryCreateOrConnectWithoutParentInput[]
|
||||
createMany?: Prisma.ProclamationCategoryCreateManyParentInputEnvelope
|
||||
connect?: Prisma.ProclamationCategoryWhereUniqueInput | Prisma.ProclamationCategoryWhereUniqueInput[]
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUncheckedCreateNestedManyWithoutParentInput = {
|
||||
create?: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutParentInput, Prisma.ProclamationCategoryUncheckedCreateWithoutParentInput> | Prisma.ProclamationCategoryCreateWithoutParentInput[] | Prisma.ProclamationCategoryUncheckedCreateWithoutParentInput[]
|
||||
connectOrCreate?: Prisma.ProclamationCategoryCreateOrConnectWithoutParentInput | Prisma.ProclamationCategoryCreateOrConnectWithoutParentInput[]
|
||||
createMany?: Prisma.ProclamationCategoryCreateManyParentInputEnvelope
|
||||
connect?: Prisma.ProclamationCategoryWhereUniqueInput | Prisma.ProclamationCategoryWhereUniqueInput[]
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUpdateOneWithoutChildrenNestedInput = {
|
||||
create?: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutChildrenInput, Prisma.ProclamationCategoryUncheckedCreateWithoutChildrenInput>
|
||||
connectOrCreate?: Prisma.ProclamationCategoryCreateOrConnectWithoutChildrenInput
|
||||
upsert?: Prisma.ProclamationCategoryUpsertWithoutChildrenInput
|
||||
disconnect?: Prisma.ProclamationCategoryWhereInput | boolean
|
||||
delete?: Prisma.ProclamationCategoryWhereInput | boolean
|
||||
connect?: Prisma.ProclamationCategoryWhereUniqueInput
|
||||
update?: Prisma.XOR<Prisma.XOR<Prisma.ProclamationCategoryUpdateToOneWithWhereWithoutChildrenInput, Prisma.ProclamationCategoryUpdateWithoutChildrenInput>, Prisma.ProclamationCategoryUncheckedUpdateWithoutChildrenInput>
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUpdateManyWithoutParentNestedInput = {
|
||||
create?: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutParentInput, Prisma.ProclamationCategoryUncheckedCreateWithoutParentInput> | Prisma.ProclamationCategoryCreateWithoutParentInput[] | Prisma.ProclamationCategoryUncheckedCreateWithoutParentInput[]
|
||||
connectOrCreate?: Prisma.ProclamationCategoryCreateOrConnectWithoutParentInput | Prisma.ProclamationCategoryCreateOrConnectWithoutParentInput[]
|
||||
upsert?: Prisma.ProclamationCategoryUpsertWithWhereUniqueWithoutParentInput | Prisma.ProclamationCategoryUpsertWithWhereUniqueWithoutParentInput[]
|
||||
createMany?: Prisma.ProclamationCategoryCreateManyParentInputEnvelope
|
||||
set?: Prisma.ProclamationCategoryWhereUniqueInput | Prisma.ProclamationCategoryWhereUniqueInput[]
|
||||
disconnect?: Prisma.ProclamationCategoryWhereUniqueInput | Prisma.ProclamationCategoryWhereUniqueInput[]
|
||||
delete?: Prisma.ProclamationCategoryWhereUniqueInput | Prisma.ProclamationCategoryWhereUniqueInput[]
|
||||
connect?: Prisma.ProclamationCategoryWhereUniqueInput | Prisma.ProclamationCategoryWhereUniqueInput[]
|
||||
update?: Prisma.ProclamationCategoryUpdateWithWhereUniqueWithoutParentInput | Prisma.ProclamationCategoryUpdateWithWhereUniqueWithoutParentInput[]
|
||||
updateMany?: Prisma.ProclamationCategoryUpdateManyWithWhereWithoutParentInput | Prisma.ProclamationCategoryUpdateManyWithWhereWithoutParentInput[]
|
||||
deleteMany?: Prisma.ProclamationCategoryScalarWhereInput | Prisma.ProclamationCategoryScalarWhereInput[]
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUncheckedUpdateManyWithoutParentNestedInput = {
|
||||
create?: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutParentInput, Prisma.ProclamationCategoryUncheckedCreateWithoutParentInput> | Prisma.ProclamationCategoryCreateWithoutParentInput[] | Prisma.ProclamationCategoryUncheckedCreateWithoutParentInput[]
|
||||
connectOrCreate?: Prisma.ProclamationCategoryCreateOrConnectWithoutParentInput | Prisma.ProclamationCategoryCreateOrConnectWithoutParentInput[]
|
||||
upsert?: Prisma.ProclamationCategoryUpsertWithWhereUniqueWithoutParentInput | Prisma.ProclamationCategoryUpsertWithWhereUniqueWithoutParentInput[]
|
||||
createMany?: Prisma.ProclamationCategoryCreateManyParentInputEnvelope
|
||||
set?: Prisma.ProclamationCategoryWhereUniqueInput | Prisma.ProclamationCategoryWhereUniqueInput[]
|
||||
disconnect?: Prisma.ProclamationCategoryWhereUniqueInput | Prisma.ProclamationCategoryWhereUniqueInput[]
|
||||
delete?: Prisma.ProclamationCategoryWhereUniqueInput | Prisma.ProclamationCategoryWhereUniqueInput[]
|
||||
connect?: Prisma.ProclamationCategoryWhereUniqueInput | Prisma.ProclamationCategoryWhereUniqueInput[]
|
||||
update?: Prisma.ProclamationCategoryUpdateWithWhereUniqueWithoutParentInput | Prisma.ProclamationCategoryUpdateWithWhereUniqueWithoutParentInput[]
|
||||
updateMany?: Prisma.ProclamationCategoryUpdateManyWithWhereWithoutParentInput | Prisma.ProclamationCategoryUpdateManyWithWhereWithoutParentInput[]
|
||||
deleteMany?: Prisma.ProclamationCategoryScalarWhereInput | Prisma.ProclamationCategoryScalarWhereInput[]
|
||||
}
|
||||
|
||||
export type ProclamationCategoryCreateNestedOneWithoutSubscriptionLevelsInput = {
|
||||
create?: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutSubscriptionLevelsInput, Prisma.ProclamationCategoryUncheckedCreateWithoutSubscriptionLevelsInput>
|
||||
connectOrCreate?: Prisma.ProclamationCategoryCreateOrConnectWithoutSubscriptionLevelsInput
|
||||
connect?: Prisma.ProclamationCategoryWhereUniqueInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUpdateOneRequiredWithoutSubscriptionLevelsNestedInput = {
|
||||
create?: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutSubscriptionLevelsInput, Prisma.ProclamationCategoryUncheckedCreateWithoutSubscriptionLevelsInput>
|
||||
connectOrCreate?: Prisma.ProclamationCategoryCreateOrConnectWithoutSubscriptionLevelsInput
|
||||
upsert?: Prisma.ProclamationCategoryUpsertWithoutSubscriptionLevelsInput
|
||||
connect?: Prisma.ProclamationCategoryWhereUniqueInput
|
||||
update?: Prisma.XOR<Prisma.XOR<Prisma.ProclamationCategoryUpdateToOneWithWhereWithoutSubscriptionLevelsInput, Prisma.ProclamationCategoryUpdateWithoutSubscriptionLevelsInput>, Prisma.ProclamationCategoryUncheckedUpdateWithoutSubscriptionLevelsInput>
|
||||
}
|
||||
|
||||
export type ProclamationCategoryCreateWithoutProclamationsInput = {
|
||||
name: string
|
||||
sortOrder?: number
|
||||
parent?: Prisma.ProclamationCategoryCreateNestedOneWithoutChildrenInput
|
||||
children?: Prisma.ProclamationCategoryCreateNestedManyWithoutParentInput
|
||||
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelCreateNestedManyWithoutCategoryInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUncheckedCreateWithoutProclamationsInput = {
|
||||
id?: number
|
||||
name: string
|
||||
sortOrder?: number
|
||||
parentId?: number | null
|
||||
children?: Prisma.ProclamationCategoryUncheckedCreateNestedManyWithoutParentInput
|
||||
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUncheckedCreateNestedManyWithoutCategoryInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryCreateOrConnectWithoutProclamationsInput = {
|
||||
@@ -507,200 +364,12 @@ export type ProclamationCategoryUpdateToOneWithWhereWithoutProclamationsInput =
|
||||
export type ProclamationCategoryUpdateWithoutProclamationsInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
parent?: Prisma.ProclamationCategoryUpdateOneWithoutChildrenNestedInput
|
||||
children?: Prisma.ProclamationCategoryUpdateManyWithoutParentNestedInput
|
||||
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUpdateManyWithoutCategoryNestedInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUncheckedUpdateWithoutProclamationsInput = {
|
||||
id?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
parentId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
children?: Prisma.ProclamationCategoryUncheckedUpdateManyWithoutParentNestedInput
|
||||
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUncheckedUpdateManyWithoutCategoryNestedInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryCreateWithoutChildrenInput = {
|
||||
name: string
|
||||
sortOrder?: number
|
||||
parent?: Prisma.ProclamationCategoryCreateNestedOneWithoutChildrenInput
|
||||
proclamations?: Prisma.ProclamationCreateNestedManyWithoutCategoryInput
|
||||
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelCreateNestedManyWithoutCategoryInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUncheckedCreateWithoutChildrenInput = {
|
||||
id?: number
|
||||
name: string
|
||||
sortOrder?: number
|
||||
parentId?: number | null
|
||||
proclamations?: Prisma.ProclamationUncheckedCreateNestedManyWithoutCategoryInput
|
||||
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUncheckedCreateNestedManyWithoutCategoryInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryCreateOrConnectWithoutChildrenInput = {
|
||||
where: Prisma.ProclamationCategoryWhereUniqueInput
|
||||
create: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutChildrenInput, Prisma.ProclamationCategoryUncheckedCreateWithoutChildrenInput>
|
||||
}
|
||||
|
||||
export type ProclamationCategoryCreateWithoutParentInput = {
|
||||
name: string
|
||||
sortOrder?: number
|
||||
children?: Prisma.ProclamationCategoryCreateNestedManyWithoutParentInput
|
||||
proclamations?: Prisma.ProclamationCreateNestedManyWithoutCategoryInput
|
||||
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelCreateNestedManyWithoutCategoryInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUncheckedCreateWithoutParentInput = {
|
||||
id?: number
|
||||
name: string
|
||||
sortOrder?: number
|
||||
children?: Prisma.ProclamationCategoryUncheckedCreateNestedManyWithoutParentInput
|
||||
proclamations?: Prisma.ProclamationUncheckedCreateNestedManyWithoutCategoryInput
|
||||
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUncheckedCreateNestedManyWithoutCategoryInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryCreateOrConnectWithoutParentInput = {
|
||||
where: Prisma.ProclamationCategoryWhereUniqueInput
|
||||
create: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutParentInput, Prisma.ProclamationCategoryUncheckedCreateWithoutParentInput>
|
||||
}
|
||||
|
||||
export type ProclamationCategoryCreateManyParentInputEnvelope = {
|
||||
data: Prisma.ProclamationCategoryCreateManyParentInput | Prisma.ProclamationCategoryCreateManyParentInput[]
|
||||
skipDuplicates?: boolean
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUpsertWithoutChildrenInput = {
|
||||
update: Prisma.XOR<Prisma.ProclamationCategoryUpdateWithoutChildrenInput, Prisma.ProclamationCategoryUncheckedUpdateWithoutChildrenInput>
|
||||
create: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutChildrenInput, Prisma.ProclamationCategoryUncheckedCreateWithoutChildrenInput>
|
||||
where?: Prisma.ProclamationCategoryWhereInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUpdateToOneWithWhereWithoutChildrenInput = {
|
||||
where?: Prisma.ProclamationCategoryWhereInput
|
||||
data: Prisma.XOR<Prisma.ProclamationCategoryUpdateWithoutChildrenInput, Prisma.ProclamationCategoryUncheckedUpdateWithoutChildrenInput>
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUpdateWithoutChildrenInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
parent?: Prisma.ProclamationCategoryUpdateOneWithoutChildrenNestedInput
|
||||
proclamations?: Prisma.ProclamationUpdateManyWithoutCategoryNestedInput
|
||||
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUpdateManyWithoutCategoryNestedInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUncheckedUpdateWithoutChildrenInput = {
|
||||
id?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
parentId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
proclamations?: Prisma.ProclamationUncheckedUpdateManyWithoutCategoryNestedInput
|
||||
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUncheckedUpdateManyWithoutCategoryNestedInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUpsertWithWhereUniqueWithoutParentInput = {
|
||||
where: Prisma.ProclamationCategoryWhereUniqueInput
|
||||
update: Prisma.XOR<Prisma.ProclamationCategoryUpdateWithoutParentInput, Prisma.ProclamationCategoryUncheckedUpdateWithoutParentInput>
|
||||
create: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutParentInput, Prisma.ProclamationCategoryUncheckedCreateWithoutParentInput>
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUpdateWithWhereUniqueWithoutParentInput = {
|
||||
where: Prisma.ProclamationCategoryWhereUniqueInput
|
||||
data: Prisma.XOR<Prisma.ProclamationCategoryUpdateWithoutParentInput, Prisma.ProclamationCategoryUncheckedUpdateWithoutParentInput>
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUpdateManyWithWhereWithoutParentInput = {
|
||||
where: Prisma.ProclamationCategoryScalarWhereInput
|
||||
data: Prisma.XOR<Prisma.ProclamationCategoryUpdateManyMutationInput, Prisma.ProclamationCategoryUncheckedUpdateManyWithoutParentInput>
|
||||
}
|
||||
|
||||
export type ProclamationCategoryScalarWhereInput = {
|
||||
AND?: Prisma.ProclamationCategoryScalarWhereInput | Prisma.ProclamationCategoryScalarWhereInput[]
|
||||
OR?: Prisma.ProclamationCategoryScalarWhereInput[]
|
||||
NOT?: Prisma.ProclamationCategoryScalarWhereInput | Prisma.ProclamationCategoryScalarWhereInput[]
|
||||
id?: Prisma.IntFilter<"ProclamationCategory"> | number
|
||||
name?: Prisma.StringFilter<"ProclamationCategory"> | string
|
||||
sortOrder?: Prisma.IntFilter<"ProclamationCategory"> | number
|
||||
parentId?: Prisma.IntNullableFilter<"ProclamationCategory"> | number | null
|
||||
}
|
||||
|
||||
export type ProclamationCategoryCreateWithoutSubscriptionLevelsInput = {
|
||||
name: string
|
||||
sortOrder?: number
|
||||
parent?: Prisma.ProclamationCategoryCreateNestedOneWithoutChildrenInput
|
||||
children?: Prisma.ProclamationCategoryCreateNestedManyWithoutParentInput
|
||||
proclamations?: Prisma.ProclamationCreateNestedManyWithoutCategoryInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUncheckedCreateWithoutSubscriptionLevelsInput = {
|
||||
id?: number
|
||||
name: string
|
||||
sortOrder?: number
|
||||
parentId?: number | null
|
||||
children?: Prisma.ProclamationCategoryUncheckedCreateNestedManyWithoutParentInput
|
||||
proclamations?: Prisma.ProclamationUncheckedCreateNestedManyWithoutCategoryInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryCreateOrConnectWithoutSubscriptionLevelsInput = {
|
||||
where: Prisma.ProclamationCategoryWhereUniqueInput
|
||||
create: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutSubscriptionLevelsInput, Prisma.ProclamationCategoryUncheckedCreateWithoutSubscriptionLevelsInput>
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUpsertWithoutSubscriptionLevelsInput = {
|
||||
update: Prisma.XOR<Prisma.ProclamationCategoryUpdateWithoutSubscriptionLevelsInput, Prisma.ProclamationCategoryUncheckedUpdateWithoutSubscriptionLevelsInput>
|
||||
create: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutSubscriptionLevelsInput, Prisma.ProclamationCategoryUncheckedCreateWithoutSubscriptionLevelsInput>
|
||||
where?: Prisma.ProclamationCategoryWhereInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUpdateToOneWithWhereWithoutSubscriptionLevelsInput = {
|
||||
where?: Prisma.ProclamationCategoryWhereInput
|
||||
data: Prisma.XOR<Prisma.ProclamationCategoryUpdateWithoutSubscriptionLevelsInput, Prisma.ProclamationCategoryUncheckedUpdateWithoutSubscriptionLevelsInput>
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUpdateWithoutSubscriptionLevelsInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
parent?: Prisma.ProclamationCategoryUpdateOneWithoutChildrenNestedInput
|
||||
children?: Prisma.ProclamationCategoryUpdateManyWithoutParentNestedInput
|
||||
proclamations?: Prisma.ProclamationUpdateManyWithoutCategoryNestedInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUncheckedUpdateWithoutSubscriptionLevelsInput = {
|
||||
id?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
parentId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
children?: Prisma.ProclamationCategoryUncheckedUpdateManyWithoutParentNestedInput
|
||||
proclamations?: Prisma.ProclamationUncheckedUpdateManyWithoutCategoryNestedInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryCreateManyParentInput = {
|
||||
id?: number
|
||||
name: string
|
||||
sortOrder?: number
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUpdateWithoutParentInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
children?: Prisma.ProclamationCategoryUpdateManyWithoutParentNestedInput
|
||||
proclamations?: Prisma.ProclamationUpdateManyWithoutCategoryNestedInput
|
||||
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUpdateManyWithoutCategoryNestedInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUncheckedUpdateWithoutParentInput = {
|
||||
id?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
children?: Prisma.ProclamationCategoryUncheckedUpdateManyWithoutParentNestedInput
|
||||
proclamations?: Prisma.ProclamationUncheckedUpdateManyWithoutCategoryNestedInput
|
||||
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUncheckedUpdateManyWithoutCategoryNestedInput
|
||||
}
|
||||
|
||||
export type ProclamationCategoryUncheckedUpdateManyWithoutParentInput = {
|
||||
id?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
}
|
||||
|
||||
|
||||
@@ -709,15 +378,11 @@ export type ProclamationCategoryUncheckedUpdateManyWithoutParentInput = {
|
||||
*/
|
||||
|
||||
export type ProclamationCategoryCountOutputType = {
|
||||
children: number
|
||||
proclamations: number
|
||||
subscriptionLevels: number
|
||||
}
|
||||
|
||||
export type ProclamationCategoryCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
children?: boolean | ProclamationCategoryCountOutputTypeCountChildrenArgs
|
||||
proclamations?: boolean | ProclamationCategoryCountOutputTypeCountProclamationsArgs
|
||||
subscriptionLevels?: boolean | ProclamationCategoryCountOutputTypeCountSubscriptionLevelsArgs
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -730,13 +395,6 @@ export type ProclamationCategoryCountOutputTypeDefaultArgs<ExtArgs extends runti
|
||||
select?: Prisma.ProclamationCategoryCountOutputTypeSelect<ExtArgs> | null
|
||||
}
|
||||
|
||||
/**
|
||||
* ProclamationCategoryCountOutputType without action
|
||||
*/
|
||||
export type ProclamationCategoryCountOutputTypeCountChildrenArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
where?: Prisma.ProclamationCategoryWhereInput
|
||||
}
|
||||
|
||||
/**
|
||||
* ProclamationCategoryCountOutputType without action
|
||||
*/
|
||||
@@ -744,23 +402,12 @@ export type ProclamationCategoryCountOutputTypeCountProclamationsArgs<ExtArgs ex
|
||||
where?: Prisma.ProclamationWhereInput
|
||||
}
|
||||
|
||||
/**
|
||||
* ProclamationCategoryCountOutputType without action
|
||||
*/
|
||||
export type ProclamationCategoryCountOutputTypeCountSubscriptionLevelsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
where?: Prisma.ProclamationCategorySubscriptionLevelWhereInput
|
||||
}
|
||||
|
||||
|
||||
export type ProclamationCategorySelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||
id?: boolean
|
||||
name?: boolean
|
||||
sortOrder?: boolean
|
||||
parentId?: boolean
|
||||
parent?: boolean | Prisma.ProclamationCategory$parentArgs<ExtArgs>
|
||||
children?: boolean | Prisma.ProclamationCategory$childrenArgs<ExtArgs>
|
||||
proclamations?: boolean | Prisma.ProclamationCategory$proclamationsArgs<ExtArgs>
|
||||
subscriptionLevels?: boolean | Prisma.ProclamationCategory$subscriptionLevelsArgs<ExtArgs>
|
||||
_count?: boolean | Prisma.ProclamationCategoryCountOutputTypeDefaultArgs<ExtArgs>
|
||||
}, ExtArgs["result"]["proclamationCategory"]>
|
||||
|
||||
@@ -768,53 +415,37 @@ export type ProclamationCategorySelectCreateManyAndReturn<ExtArgs extends runtim
|
||||
id?: boolean
|
||||
name?: boolean
|
||||
sortOrder?: boolean
|
||||
parentId?: boolean
|
||||
parent?: boolean | Prisma.ProclamationCategory$parentArgs<ExtArgs>
|
||||
}, ExtArgs["result"]["proclamationCategory"]>
|
||||
|
||||
export type ProclamationCategorySelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||
id?: boolean
|
||||
name?: boolean
|
||||
sortOrder?: boolean
|
||||
parentId?: boolean
|
||||
parent?: boolean | Prisma.ProclamationCategory$parentArgs<ExtArgs>
|
||||
}, ExtArgs["result"]["proclamationCategory"]>
|
||||
|
||||
export type ProclamationCategorySelectScalar = {
|
||||
id?: boolean
|
||||
name?: boolean
|
||||
sortOrder?: boolean
|
||||
parentId?: boolean
|
||||
}
|
||||
|
||||
export type ProclamationCategoryOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "sortOrder" | "parentId", ExtArgs["result"]["proclamationCategory"]>
|
||||
export type ProclamationCategoryOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "sortOrder", ExtArgs["result"]["proclamationCategory"]>
|
||||
export type ProclamationCategoryInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
parent?: boolean | Prisma.ProclamationCategory$parentArgs<ExtArgs>
|
||||
children?: boolean | Prisma.ProclamationCategory$childrenArgs<ExtArgs>
|
||||
proclamations?: boolean | Prisma.ProclamationCategory$proclamationsArgs<ExtArgs>
|
||||
subscriptionLevels?: boolean | Prisma.ProclamationCategory$subscriptionLevelsArgs<ExtArgs>
|
||||
_count?: boolean | Prisma.ProclamationCategoryCountOutputTypeDefaultArgs<ExtArgs>
|
||||
}
|
||||
export type ProclamationCategoryIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
parent?: boolean | Prisma.ProclamationCategory$parentArgs<ExtArgs>
|
||||
}
|
||||
export type ProclamationCategoryIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
parent?: boolean | Prisma.ProclamationCategory$parentArgs<ExtArgs>
|
||||
}
|
||||
export type ProclamationCategoryIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {}
|
||||
export type ProclamationCategoryIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {}
|
||||
|
||||
export type $ProclamationCategoryPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
name: "ProclamationCategory"
|
||||
objects: {
|
||||
parent: Prisma.$ProclamationCategoryPayload<ExtArgs> | null
|
||||
children: Prisma.$ProclamationCategoryPayload<ExtArgs>[]
|
||||
proclamations: Prisma.$ProclamationPayload<ExtArgs>[]
|
||||
subscriptionLevels: Prisma.$ProclamationCategorySubscriptionLevelPayload<ExtArgs>[]
|
||||
}
|
||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||
id: number
|
||||
name: string
|
||||
sortOrder: number
|
||||
parentId: number | null
|
||||
}, ExtArgs["result"]["proclamationCategory"]>
|
||||
composites: {}
|
||||
}
|
||||
@@ -1209,10 +840,7 @@ readonly fields: ProclamationCategoryFieldRefs;
|
||||
*/
|
||||
export interface Prisma__ProclamationCategoryClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
|
||||
readonly [Symbol.toStringTag]: "PrismaPromise"
|
||||
parent<T extends Prisma.ProclamationCategory$parentArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.ProclamationCategory$parentArgs<ExtArgs>>): Prisma.Prisma__ProclamationCategoryClient<runtime.Types.Result.GetResult<Prisma.$ProclamationCategoryPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
||||
children<T extends Prisma.ProclamationCategory$childrenArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.ProclamationCategory$childrenArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ProclamationCategoryPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
proclamations<T extends Prisma.ProclamationCategory$proclamationsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.ProclamationCategory$proclamationsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ProclamationPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
subscriptionLevels<T extends Prisma.ProclamationCategory$subscriptionLevelsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.ProclamationCategory$subscriptionLevelsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ProclamationCategorySubscriptionLevelPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
/**
|
||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
||||
@@ -1245,7 +873,6 @@ export interface ProclamationCategoryFieldRefs {
|
||||
readonly id: Prisma.FieldRef<"ProclamationCategory", 'Int'>
|
||||
readonly name: Prisma.FieldRef<"ProclamationCategory", 'String'>
|
||||
readonly sortOrder: Prisma.FieldRef<"ProclamationCategory", 'Int'>
|
||||
readonly parentId: Prisma.FieldRef<"ProclamationCategory", 'Int'>
|
||||
}
|
||||
|
||||
|
||||
@@ -1500,10 +1127,6 @@ export type ProclamationCategoryCreateManyAndReturnArgs<ExtArgs extends runtime.
|
||||
*/
|
||||
data: Prisma.ProclamationCategoryCreateManyInput | Prisma.ProclamationCategoryCreateManyInput[]
|
||||
skipDuplicates?: boolean
|
||||
/**
|
||||
* Choose, which related nodes to fetch as well
|
||||
*/
|
||||
include?: Prisma.ProclamationCategoryIncludeCreateManyAndReturn<ExtArgs> | null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1574,10 +1197,6 @@ export type ProclamationCategoryUpdateManyAndReturnArgs<ExtArgs extends runtime.
|
||||
* Limit how many ProclamationCategories to update.
|
||||
*/
|
||||
limit?: number
|
||||
/**
|
||||
* Choose, which related nodes to fetch as well
|
||||
*/
|
||||
include?: Prisma.ProclamationCategoryIncludeUpdateManyAndReturn<ExtArgs> | null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1646,49 +1265,6 @@ export type ProclamationCategoryDeleteManyArgs<ExtArgs extends runtime.Types.Ext
|
||||
limit?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* ProclamationCategory.parent
|
||||
*/
|
||||
export type ProclamationCategory$parentArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
/**
|
||||
* Select specific fields to fetch from the ProclamationCategory
|
||||
*/
|
||||
select?: Prisma.ProclamationCategorySelect<ExtArgs> | null
|
||||
/**
|
||||
* Omit specific fields from the ProclamationCategory
|
||||
*/
|
||||
omit?: Prisma.ProclamationCategoryOmit<ExtArgs> | null
|
||||
/**
|
||||
* Choose, which related nodes to fetch as well
|
||||
*/
|
||||
include?: Prisma.ProclamationCategoryInclude<ExtArgs> | null
|
||||
where?: Prisma.ProclamationCategoryWhereInput
|
||||
}
|
||||
|
||||
/**
|
||||
* ProclamationCategory.children
|
||||
*/
|
||||
export type ProclamationCategory$childrenArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
/**
|
||||
* Select specific fields to fetch from the ProclamationCategory
|
||||
*/
|
||||
select?: Prisma.ProclamationCategorySelect<ExtArgs> | null
|
||||
/**
|
||||
* Omit specific fields from the ProclamationCategory
|
||||
*/
|
||||
omit?: Prisma.ProclamationCategoryOmit<ExtArgs> | null
|
||||
/**
|
||||
* Choose, which related nodes to fetch as well
|
||||
*/
|
||||
include?: Prisma.ProclamationCategoryInclude<ExtArgs> | null
|
||||
where?: Prisma.ProclamationCategoryWhereInput
|
||||
orderBy?: Prisma.ProclamationCategoryOrderByWithRelationInput | Prisma.ProclamationCategoryOrderByWithRelationInput[]
|
||||
cursor?: Prisma.ProclamationCategoryWhereUniqueInput
|
||||
take?: number
|
||||
skip?: number
|
||||
distinct?: Prisma.ProclamationCategoryScalarFieldEnum | Prisma.ProclamationCategoryScalarFieldEnum[]
|
||||
}
|
||||
|
||||
/**
|
||||
* ProclamationCategory.proclamations
|
||||
*/
|
||||
@@ -1713,30 +1289,6 @@ export type ProclamationCategory$proclamationsArgs<ExtArgs extends runtime.Types
|
||||
distinct?: Prisma.ProclamationScalarFieldEnum | Prisma.ProclamationScalarFieldEnum[]
|
||||
}
|
||||
|
||||
/**
|
||||
* ProclamationCategory.subscriptionLevels
|
||||
*/
|
||||
export type ProclamationCategory$subscriptionLevelsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
/**
|
||||
* Select specific fields to fetch from the ProclamationCategorySubscriptionLevel
|
||||
*/
|
||||
select?: Prisma.ProclamationCategorySubscriptionLevelSelect<ExtArgs> | null
|
||||
/**
|
||||
* Omit specific fields from the ProclamationCategorySubscriptionLevel
|
||||
*/
|
||||
omit?: Prisma.ProclamationCategorySubscriptionLevelOmit<ExtArgs> | null
|
||||
/**
|
||||
* Choose, which related nodes to fetch as well
|
||||
*/
|
||||
include?: Prisma.ProclamationCategorySubscriptionLevelInclude<ExtArgs> | null
|
||||
where?: Prisma.ProclamationCategorySubscriptionLevelWhereInput
|
||||
orderBy?: Prisma.ProclamationCategorySubscriptionLevelOrderByWithRelationInput | Prisma.ProclamationCategorySubscriptionLevelOrderByWithRelationInput[]
|
||||
cursor?: Prisma.ProclamationCategorySubscriptionLevelWhereUniqueInput
|
||||
take?: number
|
||||
skip?: number
|
||||
distinct?: Prisma.ProclamationCategorySubscriptionLevelScalarFieldEnum | Prisma.ProclamationCategorySubscriptionLevelScalarFieldEnum[]
|
||||
}
|
||||
|
||||
/**
|
||||
* ProclamationCategory without action
|
||||
*/
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,7 @@ export type AggregateProfile = {
|
||||
export type ProfileMinAggregateOutputType = {
|
||||
id: string | null
|
||||
userId: string | null
|
||||
email: string | null
|
||||
firstName: string | null
|
||||
lastName: string | null
|
||||
phoneNumber: string | null
|
||||
@@ -36,6 +37,7 @@ export type ProfileMinAggregateOutputType = {
|
||||
export type ProfileMaxAggregateOutputType = {
|
||||
id: string | null
|
||||
userId: string | null
|
||||
email: string | null
|
||||
firstName: string | null
|
||||
lastName: string | null
|
||||
phoneNumber: string | null
|
||||
@@ -45,6 +47,7 @@ export type ProfileMaxAggregateOutputType = {
|
||||
export type ProfileCountAggregateOutputType = {
|
||||
id: number
|
||||
userId: number
|
||||
email: number
|
||||
firstName: number
|
||||
lastName: number
|
||||
phoneNumber: number
|
||||
@@ -56,6 +59,7 @@ export type ProfileCountAggregateOutputType = {
|
||||
export type ProfileMinAggregateInputType = {
|
||||
id?: true
|
||||
userId?: true
|
||||
email?: true
|
||||
firstName?: true
|
||||
lastName?: true
|
||||
phoneNumber?: true
|
||||
@@ -65,6 +69,7 @@ export type ProfileMinAggregateInputType = {
|
||||
export type ProfileMaxAggregateInputType = {
|
||||
id?: true
|
||||
userId?: true
|
||||
email?: true
|
||||
firstName?: true
|
||||
lastName?: true
|
||||
phoneNumber?: true
|
||||
@@ -74,6 +79,7 @@ export type ProfileMaxAggregateInputType = {
|
||||
export type ProfileCountAggregateInputType = {
|
||||
id?: true
|
||||
userId?: true
|
||||
email?: true
|
||||
firstName?: true
|
||||
lastName?: true
|
||||
phoneNumber?: true
|
||||
@@ -155,7 +161,8 @@ export type ProfileGroupByArgs<ExtArgs extends runtime.Types.Extensions.Internal
|
||||
|
||||
export type ProfileGroupByOutputType = {
|
||||
id: string
|
||||
userId: string
|
||||
userId: string | null
|
||||
email: string | null
|
||||
firstName: string
|
||||
lastName: string
|
||||
phoneNumber: string
|
||||
@@ -185,18 +192,20 @@ export type ProfileWhereInput = {
|
||||
OR?: Prisma.ProfileWhereInput[]
|
||||
NOT?: Prisma.ProfileWhereInput | Prisma.ProfileWhereInput[]
|
||||
id?: Prisma.StringFilter<"Profile"> | string
|
||||
userId?: Prisma.StringFilter<"Profile"> | string
|
||||
userId?: Prisma.StringNullableFilter<"Profile"> | string | null
|
||||
email?: Prisma.StringNullableFilter<"Profile"> | string | null
|
||||
firstName?: Prisma.StringFilter<"Profile"> | string
|
||||
lastName?: Prisma.StringFilter<"Profile"> | string
|
||||
phoneNumber?: Prisma.StringFilter<"Profile"> | string
|
||||
ghlContactId?: Prisma.StringNullableFilter<"Profile"> | string | null
|
||||
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
|
||||
user?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
|
||||
addresses?: Prisma.AddressListRelationFilter
|
||||
}
|
||||
|
||||
export type ProfileOrderByWithRelationInput = {
|
||||
id?: Prisma.SortOrder
|
||||
userId?: Prisma.SortOrder
|
||||
userId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
email?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
firstName?: Prisma.SortOrder
|
||||
lastName?: Prisma.SortOrder
|
||||
phoneNumber?: Prisma.SortOrder
|
||||
@@ -207,21 +216,23 @@ export type ProfileOrderByWithRelationInput = {
|
||||
|
||||
export type ProfileWhereUniqueInput = Prisma.AtLeast<{
|
||||
id?: string
|
||||
userId?: string
|
||||
AND?: Prisma.ProfileWhereInput | Prisma.ProfileWhereInput[]
|
||||
OR?: Prisma.ProfileWhereInput[]
|
||||
NOT?: Prisma.ProfileWhereInput | Prisma.ProfileWhereInput[]
|
||||
userId?: Prisma.StringNullableFilter<"Profile"> | string | null
|
||||
email?: Prisma.StringNullableFilter<"Profile"> | string | null
|
||||
firstName?: Prisma.StringFilter<"Profile"> | string
|
||||
lastName?: Prisma.StringFilter<"Profile"> | string
|
||||
phoneNumber?: Prisma.StringFilter<"Profile"> | string
|
||||
ghlContactId?: Prisma.StringNullableFilter<"Profile"> | string | null
|
||||
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
|
||||
user?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
|
||||
addresses?: Prisma.AddressListRelationFilter
|
||||
}, "id" | "userId">
|
||||
}, "id">
|
||||
|
||||
export type ProfileOrderByWithAggregationInput = {
|
||||
id?: Prisma.SortOrder
|
||||
userId?: Prisma.SortOrder
|
||||
userId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
email?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
firstName?: Prisma.SortOrder
|
||||
lastName?: Prisma.SortOrder
|
||||
phoneNumber?: Prisma.SortOrder
|
||||
@@ -236,7 +247,8 @@ export type ProfileScalarWhereWithAggregatesInput = {
|
||||
OR?: Prisma.ProfileScalarWhereWithAggregatesInput[]
|
||||
NOT?: Prisma.ProfileScalarWhereWithAggregatesInput | Prisma.ProfileScalarWhereWithAggregatesInput[]
|
||||
id?: Prisma.StringWithAggregatesFilter<"Profile"> | string
|
||||
userId?: Prisma.StringWithAggregatesFilter<"Profile"> | string
|
||||
userId?: Prisma.StringNullableWithAggregatesFilter<"Profile"> | string | null
|
||||
email?: Prisma.StringNullableWithAggregatesFilter<"Profile"> | string | null
|
||||
firstName?: Prisma.StringWithAggregatesFilter<"Profile"> | string
|
||||
lastName?: Prisma.StringWithAggregatesFilter<"Profile"> | string
|
||||
phoneNumber?: Prisma.StringWithAggregatesFilter<"Profile"> | string
|
||||
@@ -245,17 +257,19 @@ export type ProfileScalarWhereWithAggregatesInput = {
|
||||
|
||||
export type ProfileCreateInput = {
|
||||
id?: string
|
||||
email?: string | null
|
||||
firstName: string
|
||||
lastName: string
|
||||
phoneNumber: string
|
||||
ghlContactId?: string | null
|
||||
user: Prisma.UserCreateNestedOneWithoutProfileInput
|
||||
user?: Prisma.UserCreateNestedOneWithoutProfilesInput
|
||||
addresses?: Prisma.AddressCreateNestedManyWithoutProfileInput
|
||||
}
|
||||
|
||||
export type ProfileUncheckedCreateInput = {
|
||||
id?: string
|
||||
userId: string
|
||||
userId?: string | null
|
||||
email?: string | null
|
||||
firstName: string
|
||||
lastName: string
|
||||
phoneNumber: string
|
||||
@@ -265,17 +279,19 @@ export type ProfileUncheckedCreateInput = {
|
||||
|
||||
export type ProfileUpdateInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
firstName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
lastName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
user?: Prisma.UserUpdateOneRequiredWithoutProfileNestedInput
|
||||
user?: Prisma.UserUpdateOneWithoutProfilesNestedInput
|
||||
addresses?: Prisma.AddressUpdateManyWithoutProfileNestedInput
|
||||
}
|
||||
|
||||
export type ProfileUncheckedUpdateInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
firstName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
lastName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
@@ -285,7 +301,8 @@ export type ProfileUncheckedUpdateInput = {
|
||||
|
||||
export type ProfileCreateManyInput = {
|
||||
id?: string
|
||||
userId: string
|
||||
userId?: string | null
|
||||
email?: string | null
|
||||
firstName: string
|
||||
lastName: string
|
||||
phoneNumber: string
|
||||
@@ -294,6 +311,7 @@ export type ProfileCreateManyInput = {
|
||||
|
||||
export type ProfileUpdateManyMutationInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
firstName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
lastName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
@@ -302,21 +320,28 @@ export type ProfileUpdateManyMutationInput = {
|
||||
|
||||
export type ProfileUncheckedUpdateManyInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
firstName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
lastName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
}
|
||||
|
||||
export type ProfileNullableScalarRelationFilter = {
|
||||
is?: Prisma.ProfileWhereInput | null
|
||||
isNot?: Prisma.ProfileWhereInput | null
|
||||
export type ProfileListRelationFilter = {
|
||||
every?: Prisma.ProfileWhereInput
|
||||
some?: Prisma.ProfileWhereInput
|
||||
none?: Prisma.ProfileWhereInput
|
||||
}
|
||||
|
||||
export type ProfileOrderByRelationAggregateInput = {
|
||||
_count?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type ProfileCountOrderByAggregateInput = {
|
||||
id?: Prisma.SortOrder
|
||||
userId?: Prisma.SortOrder
|
||||
email?: Prisma.SortOrder
|
||||
firstName?: Prisma.SortOrder
|
||||
lastName?: Prisma.SortOrder
|
||||
phoneNumber?: Prisma.SortOrder
|
||||
@@ -326,6 +351,7 @@ export type ProfileCountOrderByAggregateInput = {
|
||||
export type ProfileMaxOrderByAggregateInput = {
|
||||
id?: Prisma.SortOrder
|
||||
userId?: Prisma.SortOrder
|
||||
email?: Prisma.SortOrder
|
||||
firstName?: Prisma.SortOrder
|
||||
lastName?: Prisma.SortOrder
|
||||
phoneNumber?: Prisma.SortOrder
|
||||
@@ -335,6 +361,7 @@ export type ProfileMaxOrderByAggregateInput = {
|
||||
export type ProfileMinOrderByAggregateInput = {
|
||||
id?: Prisma.SortOrder
|
||||
userId?: Prisma.SortOrder
|
||||
email?: Prisma.SortOrder
|
||||
firstName?: Prisma.SortOrder
|
||||
lastName?: Prisma.SortOrder
|
||||
phoneNumber?: Prisma.SortOrder
|
||||
@@ -346,36 +373,46 @@ export type ProfileScalarRelationFilter = {
|
||||
isNot?: Prisma.ProfileWhereInput
|
||||
}
|
||||
|
||||
export type ProfileCreateNestedOneWithoutUserInput = {
|
||||
create?: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput>
|
||||
connectOrCreate?: Prisma.ProfileCreateOrConnectWithoutUserInput
|
||||
connect?: Prisma.ProfileWhereUniqueInput
|
||||
export type ProfileCreateNestedManyWithoutUserInput = {
|
||||
create?: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput> | Prisma.ProfileCreateWithoutUserInput[] | Prisma.ProfileUncheckedCreateWithoutUserInput[]
|
||||
connectOrCreate?: Prisma.ProfileCreateOrConnectWithoutUserInput | Prisma.ProfileCreateOrConnectWithoutUserInput[]
|
||||
createMany?: Prisma.ProfileCreateManyUserInputEnvelope
|
||||
connect?: Prisma.ProfileWhereUniqueInput | Prisma.ProfileWhereUniqueInput[]
|
||||
}
|
||||
|
||||
export type ProfileUncheckedCreateNestedOneWithoutUserInput = {
|
||||
create?: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput>
|
||||
connectOrCreate?: Prisma.ProfileCreateOrConnectWithoutUserInput
|
||||
connect?: Prisma.ProfileWhereUniqueInput
|
||||
export type ProfileUncheckedCreateNestedManyWithoutUserInput = {
|
||||
create?: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput> | Prisma.ProfileCreateWithoutUserInput[] | Prisma.ProfileUncheckedCreateWithoutUserInput[]
|
||||
connectOrCreate?: Prisma.ProfileCreateOrConnectWithoutUserInput | Prisma.ProfileCreateOrConnectWithoutUserInput[]
|
||||
createMany?: Prisma.ProfileCreateManyUserInputEnvelope
|
||||
connect?: Prisma.ProfileWhereUniqueInput | Prisma.ProfileWhereUniqueInput[]
|
||||
}
|
||||
|
||||
export type ProfileUpdateOneWithoutUserNestedInput = {
|
||||
create?: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput>
|
||||
connectOrCreate?: Prisma.ProfileCreateOrConnectWithoutUserInput
|
||||
upsert?: Prisma.ProfileUpsertWithoutUserInput
|
||||
disconnect?: Prisma.ProfileWhereInput | boolean
|
||||
delete?: Prisma.ProfileWhereInput | boolean
|
||||
connect?: Prisma.ProfileWhereUniqueInput
|
||||
update?: Prisma.XOR<Prisma.XOR<Prisma.ProfileUpdateToOneWithWhereWithoutUserInput, Prisma.ProfileUpdateWithoutUserInput>, Prisma.ProfileUncheckedUpdateWithoutUserInput>
|
||||
export type ProfileUpdateManyWithoutUserNestedInput = {
|
||||
create?: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput> | Prisma.ProfileCreateWithoutUserInput[] | Prisma.ProfileUncheckedCreateWithoutUserInput[]
|
||||
connectOrCreate?: Prisma.ProfileCreateOrConnectWithoutUserInput | Prisma.ProfileCreateOrConnectWithoutUserInput[]
|
||||
upsert?: Prisma.ProfileUpsertWithWhereUniqueWithoutUserInput | Prisma.ProfileUpsertWithWhereUniqueWithoutUserInput[]
|
||||
createMany?: Prisma.ProfileCreateManyUserInputEnvelope
|
||||
set?: Prisma.ProfileWhereUniqueInput | Prisma.ProfileWhereUniqueInput[]
|
||||
disconnect?: Prisma.ProfileWhereUniqueInput | Prisma.ProfileWhereUniqueInput[]
|
||||
delete?: Prisma.ProfileWhereUniqueInput | Prisma.ProfileWhereUniqueInput[]
|
||||
connect?: Prisma.ProfileWhereUniqueInput | Prisma.ProfileWhereUniqueInput[]
|
||||
update?: Prisma.ProfileUpdateWithWhereUniqueWithoutUserInput | Prisma.ProfileUpdateWithWhereUniqueWithoutUserInput[]
|
||||
updateMany?: Prisma.ProfileUpdateManyWithWhereWithoutUserInput | Prisma.ProfileUpdateManyWithWhereWithoutUserInput[]
|
||||
deleteMany?: Prisma.ProfileScalarWhereInput | Prisma.ProfileScalarWhereInput[]
|
||||
}
|
||||
|
||||
export type ProfileUncheckedUpdateOneWithoutUserNestedInput = {
|
||||
create?: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput>
|
||||
connectOrCreate?: Prisma.ProfileCreateOrConnectWithoutUserInput
|
||||
upsert?: Prisma.ProfileUpsertWithoutUserInput
|
||||
disconnect?: Prisma.ProfileWhereInput | boolean
|
||||
delete?: Prisma.ProfileWhereInput | boolean
|
||||
connect?: Prisma.ProfileWhereUniqueInput
|
||||
update?: Prisma.XOR<Prisma.XOR<Prisma.ProfileUpdateToOneWithWhereWithoutUserInput, Prisma.ProfileUpdateWithoutUserInput>, Prisma.ProfileUncheckedUpdateWithoutUserInput>
|
||||
export type ProfileUncheckedUpdateManyWithoutUserNestedInput = {
|
||||
create?: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput> | Prisma.ProfileCreateWithoutUserInput[] | Prisma.ProfileUncheckedCreateWithoutUserInput[]
|
||||
connectOrCreate?: Prisma.ProfileCreateOrConnectWithoutUserInput | Prisma.ProfileCreateOrConnectWithoutUserInput[]
|
||||
upsert?: Prisma.ProfileUpsertWithWhereUniqueWithoutUserInput | Prisma.ProfileUpsertWithWhereUniqueWithoutUserInput[]
|
||||
createMany?: Prisma.ProfileCreateManyUserInputEnvelope
|
||||
set?: Prisma.ProfileWhereUniqueInput | Prisma.ProfileWhereUniqueInput[]
|
||||
disconnect?: Prisma.ProfileWhereUniqueInput | Prisma.ProfileWhereUniqueInput[]
|
||||
delete?: Prisma.ProfileWhereUniqueInput | Prisma.ProfileWhereUniqueInput[]
|
||||
connect?: Prisma.ProfileWhereUniqueInput | Prisma.ProfileWhereUniqueInput[]
|
||||
update?: Prisma.ProfileUpdateWithWhereUniqueWithoutUserInput | Prisma.ProfileUpdateWithWhereUniqueWithoutUserInput[]
|
||||
updateMany?: Prisma.ProfileUpdateManyWithWhereWithoutUserInput | Prisma.ProfileUpdateManyWithWhereWithoutUserInput[]
|
||||
deleteMany?: Prisma.ProfileScalarWhereInput | Prisma.ProfileScalarWhereInput[]
|
||||
}
|
||||
|
||||
export type ProfileCreateNestedOneWithoutAddressesInput = {
|
||||
@@ -394,6 +431,7 @@ export type ProfileUpdateOneRequiredWithoutAddressesNestedInput = {
|
||||
|
||||
export type ProfileCreateWithoutUserInput = {
|
||||
id?: string
|
||||
email?: string | null
|
||||
firstName: string
|
||||
lastName: string
|
||||
phoneNumber: string
|
||||
@@ -403,6 +441,7 @@ export type ProfileCreateWithoutUserInput = {
|
||||
|
||||
export type ProfileUncheckedCreateWithoutUserInput = {
|
||||
id?: string
|
||||
email?: string | null
|
||||
firstName: string
|
||||
lastName: string
|
||||
phoneNumber: string
|
||||
@@ -415,47 +454,54 @@ export type ProfileCreateOrConnectWithoutUserInput = {
|
||||
create: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput>
|
||||
}
|
||||
|
||||
export type ProfileUpsertWithoutUserInput = {
|
||||
update: Prisma.XOR<Prisma.ProfileUpdateWithoutUserInput, Prisma.ProfileUncheckedUpdateWithoutUserInput>
|
||||
create: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput>
|
||||
where?: Prisma.ProfileWhereInput
|
||||
export type ProfileCreateManyUserInputEnvelope = {
|
||||
data: Prisma.ProfileCreateManyUserInput | Prisma.ProfileCreateManyUserInput[]
|
||||
skipDuplicates?: boolean
|
||||
}
|
||||
|
||||
export type ProfileUpdateToOneWithWhereWithoutUserInput = {
|
||||
where?: Prisma.ProfileWhereInput
|
||||
export type ProfileUpsertWithWhereUniqueWithoutUserInput = {
|
||||
where: Prisma.ProfileWhereUniqueInput
|
||||
update: Prisma.XOR<Prisma.ProfileUpdateWithoutUserInput, Prisma.ProfileUncheckedUpdateWithoutUserInput>
|
||||
create: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput>
|
||||
}
|
||||
|
||||
export type ProfileUpdateWithWhereUniqueWithoutUserInput = {
|
||||
where: Prisma.ProfileWhereUniqueInput
|
||||
data: Prisma.XOR<Prisma.ProfileUpdateWithoutUserInput, Prisma.ProfileUncheckedUpdateWithoutUserInput>
|
||||
}
|
||||
|
||||
export type ProfileUpdateWithoutUserInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
firstName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
lastName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
addresses?: Prisma.AddressUpdateManyWithoutProfileNestedInput
|
||||
export type ProfileUpdateManyWithWhereWithoutUserInput = {
|
||||
where: Prisma.ProfileScalarWhereInput
|
||||
data: Prisma.XOR<Prisma.ProfileUpdateManyMutationInput, Prisma.ProfileUncheckedUpdateManyWithoutUserInput>
|
||||
}
|
||||
|
||||
export type ProfileUncheckedUpdateWithoutUserInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
firstName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
lastName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
addresses?: Prisma.AddressUncheckedUpdateManyWithoutProfileNestedInput
|
||||
export type ProfileScalarWhereInput = {
|
||||
AND?: Prisma.ProfileScalarWhereInput | Prisma.ProfileScalarWhereInput[]
|
||||
OR?: Prisma.ProfileScalarWhereInput[]
|
||||
NOT?: Prisma.ProfileScalarWhereInput | Prisma.ProfileScalarWhereInput[]
|
||||
id?: Prisma.StringFilter<"Profile"> | string
|
||||
userId?: Prisma.StringNullableFilter<"Profile"> | string | null
|
||||
email?: Prisma.StringNullableFilter<"Profile"> | string | null
|
||||
firstName?: Prisma.StringFilter<"Profile"> | string
|
||||
lastName?: Prisma.StringFilter<"Profile"> | string
|
||||
phoneNumber?: Prisma.StringFilter<"Profile"> | string
|
||||
ghlContactId?: Prisma.StringNullableFilter<"Profile"> | string | null
|
||||
}
|
||||
|
||||
export type ProfileCreateWithoutAddressesInput = {
|
||||
id?: string
|
||||
email?: string | null
|
||||
firstName: string
|
||||
lastName: string
|
||||
phoneNumber: string
|
||||
ghlContactId?: string | null
|
||||
user: Prisma.UserCreateNestedOneWithoutProfileInput
|
||||
user?: Prisma.UserCreateNestedOneWithoutProfilesInput
|
||||
}
|
||||
|
||||
export type ProfileUncheckedCreateWithoutAddressesInput = {
|
||||
id?: string
|
||||
userId: string
|
||||
userId?: string | null
|
||||
email?: string | null
|
||||
firstName: string
|
||||
lastName: string
|
||||
phoneNumber: string
|
||||
@@ -480,16 +526,56 @@ export type ProfileUpdateToOneWithWhereWithoutAddressesInput = {
|
||||
|
||||
export type ProfileUpdateWithoutAddressesInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
firstName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
lastName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
user?: Prisma.UserUpdateOneRequiredWithoutProfileNestedInput
|
||||
user?: Prisma.UserUpdateOneWithoutProfilesNestedInput
|
||||
}
|
||||
|
||||
export type ProfileUncheckedUpdateWithoutAddressesInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
firstName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
lastName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
}
|
||||
|
||||
export type ProfileCreateManyUserInput = {
|
||||
id?: string
|
||||
email?: string | null
|
||||
firstName: string
|
||||
lastName: string
|
||||
phoneNumber: string
|
||||
ghlContactId?: string | null
|
||||
}
|
||||
|
||||
export type ProfileUpdateWithoutUserInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
firstName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
lastName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
addresses?: Prisma.AddressUpdateManyWithoutProfileNestedInput
|
||||
}
|
||||
|
||||
export type ProfileUncheckedUpdateWithoutUserInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
firstName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
lastName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
addresses?: Prisma.AddressUncheckedUpdateManyWithoutProfileNestedInput
|
||||
}
|
||||
|
||||
export type ProfileUncheckedUpdateManyWithoutUserInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
firstName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
lastName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
@@ -530,11 +616,12 @@ export type ProfileCountOutputTypeCountAddressesArgs<ExtArgs extends runtime.Typ
|
||||
export type ProfileSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||
id?: boolean
|
||||
userId?: boolean
|
||||
email?: boolean
|
||||
firstName?: boolean
|
||||
lastName?: boolean
|
||||
phoneNumber?: boolean
|
||||
ghlContactId?: boolean
|
||||
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
||||
user?: boolean | Prisma.Profile$userArgs<ExtArgs>
|
||||
addresses?: boolean | Prisma.Profile$addressesArgs<ExtArgs>
|
||||
_count?: boolean | Prisma.ProfileCountOutputTypeDefaultArgs<ExtArgs>
|
||||
}, ExtArgs["result"]["profile"]>
|
||||
@@ -542,54 +629,58 @@ export type ProfileSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
||||
export type ProfileSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||
id?: boolean
|
||||
userId?: boolean
|
||||
email?: boolean
|
||||
firstName?: boolean
|
||||
lastName?: boolean
|
||||
phoneNumber?: boolean
|
||||
ghlContactId?: boolean
|
||||
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
||||
user?: boolean | Prisma.Profile$userArgs<ExtArgs>
|
||||
}, ExtArgs["result"]["profile"]>
|
||||
|
||||
export type ProfileSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||
id?: boolean
|
||||
userId?: boolean
|
||||
email?: boolean
|
||||
firstName?: boolean
|
||||
lastName?: boolean
|
||||
phoneNumber?: boolean
|
||||
ghlContactId?: boolean
|
||||
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
||||
user?: boolean | Prisma.Profile$userArgs<ExtArgs>
|
||||
}, ExtArgs["result"]["profile"]>
|
||||
|
||||
export type ProfileSelectScalar = {
|
||||
id?: boolean
|
||||
userId?: boolean
|
||||
email?: boolean
|
||||
firstName?: boolean
|
||||
lastName?: boolean
|
||||
phoneNumber?: boolean
|
||||
ghlContactId?: boolean
|
||||
}
|
||||
|
||||
export type ProfileOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "userId" | "firstName" | "lastName" | "phoneNumber" | "ghlContactId", ExtArgs["result"]["profile"]>
|
||||
export type ProfileOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "userId" | "email" | "firstName" | "lastName" | "phoneNumber" | "ghlContactId", ExtArgs["result"]["profile"]>
|
||||
export type ProfileInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
||||
user?: boolean | Prisma.Profile$userArgs<ExtArgs>
|
||||
addresses?: boolean | Prisma.Profile$addressesArgs<ExtArgs>
|
||||
_count?: boolean | Prisma.ProfileCountOutputTypeDefaultArgs<ExtArgs>
|
||||
}
|
||||
export type ProfileIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
||||
user?: boolean | Prisma.Profile$userArgs<ExtArgs>
|
||||
}
|
||||
export type ProfileIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
||||
user?: boolean | Prisma.Profile$userArgs<ExtArgs>
|
||||
}
|
||||
|
||||
export type $ProfilePayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
name: "Profile"
|
||||
objects: {
|
||||
user: Prisma.$UserPayload<ExtArgs>
|
||||
user: Prisma.$UserPayload<ExtArgs> | null
|
||||
addresses: Prisma.$AddressPayload<ExtArgs>[]
|
||||
}
|
||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||
id: string
|
||||
userId: string
|
||||
userId: string | null
|
||||
email: string | null
|
||||
firstName: string
|
||||
lastName: string
|
||||
phoneNumber: string
|
||||
@@ -988,7 +1079,7 @@ readonly fields: ProfileFieldRefs;
|
||||
*/
|
||||
export interface Prisma__ProfileClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
|
||||
readonly [Symbol.toStringTag]: "PrismaPromise"
|
||||
user<T extends Prisma.UserDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.UserDefaultArgs<ExtArgs>>): Prisma.Prisma__UserClient<runtime.Types.Result.GetResult<Prisma.$UserPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
|
||||
user<T extends Prisma.Profile$userArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Profile$userArgs<ExtArgs>>): Prisma.Prisma__UserClient<runtime.Types.Result.GetResult<Prisma.$UserPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
||||
addresses<T extends Prisma.Profile$addressesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Profile$addressesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$AddressPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
/**
|
||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||
@@ -1021,6 +1112,7 @@ export interface Prisma__ProfileClient<T, Null = never, ExtArgs extends runtime.
|
||||
export interface ProfileFieldRefs {
|
||||
readonly id: Prisma.FieldRef<"Profile", 'String'>
|
||||
readonly userId: Prisma.FieldRef<"Profile", 'String'>
|
||||
readonly email: Prisma.FieldRef<"Profile", 'String'>
|
||||
readonly firstName: Prisma.FieldRef<"Profile", 'String'>
|
||||
readonly lastName: Prisma.FieldRef<"Profile", 'String'>
|
||||
readonly phoneNumber: Prisma.FieldRef<"Profile", 'String'>
|
||||
@@ -1425,6 +1517,25 @@ export type ProfileDeleteManyArgs<ExtArgs extends runtime.Types.Extensions.Inter
|
||||
limit?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Profile.user
|
||||
*/
|
||||
export type Profile$userArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
/**
|
||||
* Select specific fields to fetch from the User
|
||||
*/
|
||||
select?: Prisma.UserSelect<ExtArgs> | null
|
||||
/**
|
||||
* Omit specific fields from the User
|
||||
*/
|
||||
omit?: Prisma.UserOmit<ExtArgs> | null
|
||||
/**
|
||||
* Choose, which related nodes to fetch as well
|
||||
*/
|
||||
include?: Prisma.UserInclude<ExtArgs> | null
|
||||
where?: Prisma.UserWhereInput
|
||||
}
|
||||
|
||||
/**
|
||||
* Profile.addresses
|
||||
*/
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -243,7 +243,7 @@ export type UserWhereInput = {
|
||||
role?: Prisma.XOR<Prisma.UserRoleNullableScalarRelationFilter, Prisma.UserRoleWhereInput> | null
|
||||
sessions?: Prisma.SessionListRelationFilter
|
||||
accounts?: Prisma.AccountListRelationFilter
|
||||
profile?: Prisma.XOR<Prisma.ProfileNullableScalarRelationFilter, Prisma.ProfileWhereInput> | null
|
||||
profiles?: Prisma.ProfileListRelationFilter
|
||||
licenseKeys?: Prisma.LicenseKeyListRelationFilter
|
||||
userProclamations?: Prisma.UserProclamationsListRelationFilter
|
||||
}
|
||||
@@ -260,7 +260,7 @@ export type UserOrderByWithRelationInput = {
|
||||
role?: Prisma.UserRoleOrderByWithRelationInput
|
||||
sessions?: Prisma.SessionOrderByRelationAggregateInput
|
||||
accounts?: Prisma.AccountOrderByRelationAggregateInput
|
||||
profile?: Prisma.ProfileOrderByWithRelationInput
|
||||
profiles?: Prisma.ProfileOrderByRelationAggregateInput
|
||||
licenseKeys?: Prisma.LicenseKeyOrderByRelationAggregateInput
|
||||
userProclamations?: Prisma.UserProclamationsOrderByRelationAggregateInput
|
||||
}
|
||||
@@ -280,7 +280,7 @@ export type UserWhereUniqueInput = Prisma.AtLeast<{
|
||||
role?: Prisma.XOR<Prisma.UserRoleNullableScalarRelationFilter, Prisma.UserRoleWhereInput> | null
|
||||
sessions?: Prisma.SessionListRelationFilter
|
||||
accounts?: Prisma.AccountListRelationFilter
|
||||
profile?: Prisma.XOR<Prisma.ProfileNullableScalarRelationFilter, Prisma.ProfileWhereInput> | null
|
||||
profiles?: Prisma.ProfileListRelationFilter
|
||||
licenseKeys?: Prisma.LicenseKeyListRelationFilter
|
||||
userProclamations?: Prisma.UserProclamationsListRelationFilter
|
||||
}, "id" | "email">
|
||||
@@ -326,7 +326,7 @@ export type UserCreateInput = {
|
||||
role?: Prisma.UserRoleCreateNestedOneWithoutUsersInput
|
||||
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
|
||||
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
|
||||
profile?: Prisma.ProfileCreateNestedOneWithoutUserInput
|
||||
profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput
|
||||
licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput
|
||||
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput
|
||||
}
|
||||
@@ -342,7 +342,7 @@ export type UserUncheckedCreateInput = {
|
||||
roleId?: number | null
|
||||
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
|
||||
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
|
||||
profile?: Prisma.ProfileUncheckedCreateNestedOneWithoutUserInput
|
||||
profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput
|
||||
licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput
|
||||
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput
|
||||
}
|
||||
@@ -358,7 +358,7 @@ export type UserUpdateInput = {
|
||||
role?: Prisma.UserRoleUpdateOneWithoutUsersNestedInput
|
||||
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
|
||||
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
|
||||
profile?: Prisma.ProfileUpdateOneWithoutUserNestedInput
|
||||
profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput
|
||||
licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput
|
||||
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput
|
||||
}
|
||||
@@ -374,7 +374,7 @@ export type UserUncheckedUpdateInput = {
|
||||
roleId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
|
||||
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
|
||||
profile?: Prisma.ProfileUncheckedUpdateOneWithoutUserNestedInput
|
||||
profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput
|
||||
licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput
|
||||
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput
|
||||
}
|
||||
@@ -452,6 +452,11 @@ export type UserSumOrderByAggregateInput = {
|
||||
roleId?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type UserNullableScalarRelationFilter = {
|
||||
is?: Prisma.UserWhereInput | null
|
||||
isNot?: Prisma.UserWhereInput | null
|
||||
}
|
||||
|
||||
export type UserScalarRelationFilter = {
|
||||
is?: Prisma.UserWhereInput
|
||||
isNot?: Prisma.UserWhereInput
|
||||
@@ -491,18 +496,20 @@ export type NullableIntFieldUpdateOperationsInput = {
|
||||
divide?: number
|
||||
}
|
||||
|
||||
export type UserCreateNestedOneWithoutProfileInput = {
|
||||
create?: Prisma.XOR<Prisma.UserCreateWithoutProfileInput, Prisma.UserUncheckedCreateWithoutProfileInput>
|
||||
connectOrCreate?: Prisma.UserCreateOrConnectWithoutProfileInput
|
||||
export type UserCreateNestedOneWithoutProfilesInput = {
|
||||
create?: Prisma.XOR<Prisma.UserCreateWithoutProfilesInput, Prisma.UserUncheckedCreateWithoutProfilesInput>
|
||||
connectOrCreate?: Prisma.UserCreateOrConnectWithoutProfilesInput
|
||||
connect?: Prisma.UserWhereUniqueInput
|
||||
}
|
||||
|
||||
export type UserUpdateOneRequiredWithoutProfileNestedInput = {
|
||||
create?: Prisma.XOR<Prisma.UserCreateWithoutProfileInput, Prisma.UserUncheckedCreateWithoutProfileInput>
|
||||
connectOrCreate?: Prisma.UserCreateOrConnectWithoutProfileInput
|
||||
upsert?: Prisma.UserUpsertWithoutProfileInput
|
||||
export type UserUpdateOneWithoutProfilesNestedInput = {
|
||||
create?: Prisma.XOR<Prisma.UserCreateWithoutProfilesInput, Prisma.UserUncheckedCreateWithoutProfilesInput>
|
||||
connectOrCreate?: Prisma.UserCreateOrConnectWithoutProfilesInput
|
||||
upsert?: Prisma.UserUpsertWithoutProfilesInput
|
||||
disconnect?: Prisma.UserWhereInput | boolean
|
||||
delete?: Prisma.UserWhereInput | boolean
|
||||
connect?: Prisma.UserWhereUniqueInput
|
||||
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutProfileInput, Prisma.UserUpdateWithoutProfileInput>, Prisma.UserUncheckedUpdateWithoutProfileInput>
|
||||
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutProfilesInput, Prisma.UserUpdateWithoutProfilesInput>, Prisma.UserUncheckedUpdateWithoutProfilesInput>
|
||||
}
|
||||
|
||||
export type UserCreateNestedOneWithoutSessionsInput = {
|
||||
@@ -595,15 +602,17 @@ export type UserCreateNestedOneWithoutLicenseKeysInput = {
|
||||
connect?: Prisma.UserWhereUniqueInput
|
||||
}
|
||||
|
||||
export type UserUpdateOneRequiredWithoutLicenseKeysNestedInput = {
|
||||
export type UserUpdateOneWithoutLicenseKeysNestedInput = {
|
||||
create?: Prisma.XOR<Prisma.UserCreateWithoutLicenseKeysInput, Prisma.UserUncheckedCreateWithoutLicenseKeysInput>
|
||||
connectOrCreate?: Prisma.UserCreateOrConnectWithoutLicenseKeysInput
|
||||
upsert?: Prisma.UserUpsertWithoutLicenseKeysInput
|
||||
disconnect?: Prisma.UserWhereInput | boolean
|
||||
delete?: Prisma.UserWhereInput | boolean
|
||||
connect?: Prisma.UserWhereUniqueInput
|
||||
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutLicenseKeysInput, Prisma.UserUpdateWithoutLicenseKeysInput>, Prisma.UserUncheckedUpdateWithoutLicenseKeysInput>
|
||||
}
|
||||
|
||||
export type UserCreateWithoutProfileInput = {
|
||||
export type UserCreateWithoutProfilesInput = {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
@@ -618,7 +627,7 @@ export type UserCreateWithoutProfileInput = {
|
||||
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput
|
||||
}
|
||||
|
||||
export type UserUncheckedCreateWithoutProfileInput = {
|
||||
export type UserUncheckedCreateWithoutProfilesInput = {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
@@ -633,23 +642,23 @@ export type UserUncheckedCreateWithoutProfileInput = {
|
||||
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput
|
||||
}
|
||||
|
||||
export type UserCreateOrConnectWithoutProfileInput = {
|
||||
export type UserCreateOrConnectWithoutProfilesInput = {
|
||||
where: Prisma.UserWhereUniqueInput
|
||||
create: Prisma.XOR<Prisma.UserCreateWithoutProfileInput, Prisma.UserUncheckedCreateWithoutProfileInput>
|
||||
create: Prisma.XOR<Prisma.UserCreateWithoutProfilesInput, Prisma.UserUncheckedCreateWithoutProfilesInput>
|
||||
}
|
||||
|
||||
export type UserUpsertWithoutProfileInput = {
|
||||
update: Prisma.XOR<Prisma.UserUpdateWithoutProfileInput, Prisma.UserUncheckedUpdateWithoutProfileInput>
|
||||
create: Prisma.XOR<Prisma.UserCreateWithoutProfileInput, Prisma.UserUncheckedCreateWithoutProfileInput>
|
||||
export type UserUpsertWithoutProfilesInput = {
|
||||
update: Prisma.XOR<Prisma.UserUpdateWithoutProfilesInput, Prisma.UserUncheckedUpdateWithoutProfilesInput>
|
||||
create: Prisma.XOR<Prisma.UserCreateWithoutProfilesInput, Prisma.UserUncheckedCreateWithoutProfilesInput>
|
||||
where?: Prisma.UserWhereInput
|
||||
}
|
||||
|
||||
export type UserUpdateToOneWithWhereWithoutProfileInput = {
|
||||
export type UserUpdateToOneWithWhereWithoutProfilesInput = {
|
||||
where?: Prisma.UserWhereInput
|
||||
data: Prisma.XOR<Prisma.UserUpdateWithoutProfileInput, Prisma.UserUncheckedUpdateWithoutProfileInput>
|
||||
data: Prisma.XOR<Prisma.UserUpdateWithoutProfilesInput, Prisma.UserUncheckedUpdateWithoutProfilesInput>
|
||||
}
|
||||
|
||||
export type UserUpdateWithoutProfileInput = {
|
||||
export type UserUpdateWithoutProfilesInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
@@ -664,7 +673,7 @@ export type UserUpdateWithoutProfileInput = {
|
||||
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput
|
||||
}
|
||||
|
||||
export type UserUncheckedUpdateWithoutProfileInput = {
|
||||
export type UserUncheckedUpdateWithoutProfilesInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
@@ -689,7 +698,7 @@ export type UserCreateWithoutSessionsInput = {
|
||||
updatedAt?: Date | string
|
||||
role?: Prisma.UserRoleCreateNestedOneWithoutUsersInput
|
||||
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
|
||||
profile?: Prisma.ProfileCreateNestedOneWithoutUserInput
|
||||
profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput
|
||||
licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput
|
||||
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput
|
||||
}
|
||||
@@ -704,7 +713,7 @@ export type UserUncheckedCreateWithoutSessionsInput = {
|
||||
updatedAt?: Date | string
|
||||
roleId?: number | null
|
||||
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
|
||||
profile?: Prisma.ProfileUncheckedCreateNestedOneWithoutUserInput
|
||||
profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput
|
||||
licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput
|
||||
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput
|
||||
}
|
||||
@@ -735,7 +744,7 @@ export type UserUpdateWithoutSessionsInput = {
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
role?: Prisma.UserRoleUpdateOneWithoutUsersNestedInput
|
||||
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
|
||||
profile?: Prisma.ProfileUpdateOneWithoutUserNestedInput
|
||||
profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput
|
||||
licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput
|
||||
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput
|
||||
}
|
||||
@@ -750,7 +759,7 @@ export type UserUncheckedUpdateWithoutSessionsInput = {
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
roleId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
|
||||
profile?: Prisma.ProfileUncheckedUpdateOneWithoutUserNestedInput
|
||||
profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput
|
||||
licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput
|
||||
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput
|
||||
}
|
||||
@@ -765,7 +774,7 @@ export type UserCreateWithoutAccountsInput = {
|
||||
updatedAt?: Date | string
|
||||
role?: Prisma.UserRoleCreateNestedOneWithoutUsersInput
|
||||
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
|
||||
profile?: Prisma.ProfileCreateNestedOneWithoutUserInput
|
||||
profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput
|
||||
licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput
|
||||
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput
|
||||
}
|
||||
@@ -780,7 +789,7 @@ export type UserUncheckedCreateWithoutAccountsInput = {
|
||||
updatedAt?: Date | string
|
||||
roleId?: number | null
|
||||
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
|
||||
profile?: Prisma.ProfileUncheckedCreateNestedOneWithoutUserInput
|
||||
profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput
|
||||
licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput
|
||||
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput
|
||||
}
|
||||
@@ -811,7 +820,7 @@ export type UserUpdateWithoutAccountsInput = {
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
role?: Prisma.UserRoleUpdateOneWithoutUsersNestedInput
|
||||
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
|
||||
profile?: Prisma.ProfileUpdateOneWithoutUserNestedInput
|
||||
profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput
|
||||
licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput
|
||||
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput
|
||||
}
|
||||
@@ -826,7 +835,7 @@ export type UserUncheckedUpdateWithoutAccountsInput = {
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
roleId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
|
||||
profile?: Prisma.ProfileUncheckedUpdateOneWithoutUserNestedInput
|
||||
profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput
|
||||
licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput
|
||||
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput
|
||||
}
|
||||
@@ -841,7 +850,7 @@ export type UserCreateWithoutRoleInput = {
|
||||
updatedAt?: Date | string
|
||||
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
|
||||
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
|
||||
profile?: Prisma.ProfileCreateNestedOneWithoutUserInput
|
||||
profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput
|
||||
licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput
|
||||
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput
|
||||
}
|
||||
@@ -856,7 +865,7 @@ export type UserUncheckedCreateWithoutRoleInput = {
|
||||
updatedAt?: Date | string
|
||||
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
|
||||
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
|
||||
profile?: Prisma.ProfileUncheckedCreateNestedOneWithoutUserInput
|
||||
profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput
|
||||
licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput
|
||||
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput
|
||||
}
|
||||
@@ -912,7 +921,7 @@ export type UserCreateWithoutUserProclamationsInput = {
|
||||
role?: Prisma.UserRoleCreateNestedOneWithoutUsersInput
|
||||
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
|
||||
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
|
||||
profile?: Prisma.ProfileCreateNestedOneWithoutUserInput
|
||||
profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput
|
||||
licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput
|
||||
}
|
||||
|
||||
@@ -927,7 +936,7 @@ export type UserUncheckedCreateWithoutUserProclamationsInput = {
|
||||
roleId?: number | null
|
||||
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
|
||||
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
|
||||
profile?: Prisma.ProfileUncheckedCreateNestedOneWithoutUserInput
|
||||
profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput
|
||||
licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput
|
||||
}
|
||||
|
||||
@@ -958,7 +967,7 @@ export type UserUpdateWithoutUserProclamationsInput = {
|
||||
role?: Prisma.UserRoleUpdateOneWithoutUsersNestedInput
|
||||
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
|
||||
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
|
||||
profile?: Prisma.ProfileUpdateOneWithoutUserNestedInput
|
||||
profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput
|
||||
licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput
|
||||
}
|
||||
|
||||
@@ -973,7 +982,7 @@ export type UserUncheckedUpdateWithoutUserProclamationsInput = {
|
||||
roleId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
|
||||
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
|
||||
profile?: Prisma.ProfileUncheckedUpdateOneWithoutUserNestedInput
|
||||
profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput
|
||||
licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput
|
||||
}
|
||||
|
||||
@@ -988,7 +997,7 @@ export type UserCreateWithoutLicenseKeysInput = {
|
||||
role?: Prisma.UserRoleCreateNestedOneWithoutUsersInput
|
||||
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
|
||||
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
|
||||
profile?: Prisma.ProfileCreateNestedOneWithoutUserInput
|
||||
profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput
|
||||
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput
|
||||
}
|
||||
|
||||
@@ -1003,7 +1012,7 @@ export type UserUncheckedCreateWithoutLicenseKeysInput = {
|
||||
roleId?: number | null
|
||||
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
|
||||
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
|
||||
profile?: Prisma.ProfileUncheckedCreateNestedOneWithoutUserInput
|
||||
profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput
|
||||
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput
|
||||
}
|
||||
|
||||
@@ -1034,7 +1043,7 @@ export type UserUpdateWithoutLicenseKeysInput = {
|
||||
role?: Prisma.UserRoleUpdateOneWithoutUsersNestedInput
|
||||
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
|
||||
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
|
||||
profile?: Prisma.ProfileUpdateOneWithoutUserNestedInput
|
||||
profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput
|
||||
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput
|
||||
}
|
||||
|
||||
@@ -1049,7 +1058,7 @@ export type UserUncheckedUpdateWithoutLicenseKeysInput = {
|
||||
roleId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
|
||||
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
|
||||
profile?: Prisma.ProfileUncheckedUpdateOneWithoutUserNestedInput
|
||||
profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput
|
||||
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput
|
||||
}
|
||||
|
||||
@@ -1073,7 +1082,7 @@ export type UserUpdateWithoutRoleInput = {
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
|
||||
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
|
||||
profile?: Prisma.ProfileUpdateOneWithoutUserNestedInput
|
||||
profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput
|
||||
licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput
|
||||
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput
|
||||
}
|
||||
@@ -1088,7 +1097,7 @@ export type UserUncheckedUpdateWithoutRoleInput = {
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
|
||||
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
|
||||
profile?: Prisma.ProfileUncheckedUpdateOneWithoutUserNestedInput
|
||||
profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput
|
||||
licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput
|
||||
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput
|
||||
}
|
||||
@@ -1111,6 +1120,7 @@ export type UserUncheckedUpdateManyWithoutRoleInput = {
|
||||
export type UserCountOutputType = {
|
||||
sessions: number
|
||||
accounts: number
|
||||
profiles: number
|
||||
licenseKeys: number
|
||||
userProclamations: number
|
||||
}
|
||||
@@ -1118,6 +1128,7 @@ export type UserCountOutputType = {
|
||||
export type UserCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
sessions?: boolean | UserCountOutputTypeCountSessionsArgs
|
||||
accounts?: boolean | UserCountOutputTypeCountAccountsArgs
|
||||
profiles?: boolean | UserCountOutputTypeCountProfilesArgs
|
||||
licenseKeys?: boolean | UserCountOutputTypeCountLicenseKeysArgs
|
||||
userProclamations?: boolean | UserCountOutputTypeCountUserProclamationsArgs
|
||||
}
|
||||
@@ -1146,6 +1157,13 @@ export type UserCountOutputTypeCountAccountsArgs<ExtArgs extends runtime.Types.E
|
||||
where?: Prisma.AccountWhereInput
|
||||
}
|
||||
|
||||
/**
|
||||
* UserCountOutputType without action
|
||||
*/
|
||||
export type UserCountOutputTypeCountProfilesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
where?: Prisma.ProfileWhereInput
|
||||
}
|
||||
|
||||
/**
|
||||
* UserCountOutputType without action
|
||||
*/
|
||||
@@ -1173,7 +1191,7 @@ export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
|
||||
role?: boolean | Prisma.User$roleArgs<ExtArgs>
|
||||
sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs>
|
||||
accounts?: boolean | Prisma.User$accountsArgs<ExtArgs>
|
||||
profile?: boolean | Prisma.User$profileArgs<ExtArgs>
|
||||
profiles?: boolean | Prisma.User$profilesArgs<ExtArgs>
|
||||
licenseKeys?: boolean | Prisma.User$licenseKeysArgs<ExtArgs>
|
||||
userProclamations?: boolean | Prisma.User$userProclamationsArgs<ExtArgs>
|
||||
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
|
||||
@@ -1219,7 +1237,7 @@ export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
||||
role?: boolean | Prisma.User$roleArgs<ExtArgs>
|
||||
sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs>
|
||||
accounts?: boolean | Prisma.User$accountsArgs<ExtArgs>
|
||||
profile?: boolean | Prisma.User$profileArgs<ExtArgs>
|
||||
profiles?: boolean | Prisma.User$profilesArgs<ExtArgs>
|
||||
licenseKeys?: boolean | Prisma.User$licenseKeysArgs<ExtArgs>
|
||||
userProclamations?: boolean | Prisma.User$userProclamationsArgs<ExtArgs>
|
||||
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
|
||||
@@ -1237,7 +1255,7 @@ export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
||||
role: Prisma.$UserRolePayload<ExtArgs> | null
|
||||
sessions: Prisma.$SessionPayload<ExtArgs>[]
|
||||
accounts: Prisma.$AccountPayload<ExtArgs>[]
|
||||
profile: Prisma.$ProfilePayload<ExtArgs> | null
|
||||
profiles: Prisma.$ProfilePayload<ExtArgs>[]
|
||||
licenseKeys: Prisma.$LicenseKeyPayload<ExtArgs>[]
|
||||
userProclamations: Prisma.$UserProclamationsPayload<ExtArgs>[]
|
||||
}
|
||||
@@ -1647,7 +1665,7 @@ export interface Prisma__UserClient<T, Null = never, ExtArgs extends runtime.Typ
|
||||
role<T extends Prisma.User$roleArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$roleArgs<ExtArgs>>): Prisma.Prisma__UserRoleClient<runtime.Types.Result.GetResult<Prisma.$UserRolePayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
||||
sessions<T extends Prisma.User$sessionsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$sessionsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$SessionPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
accounts<T extends Prisma.User$accountsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$accountsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$AccountPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
profile<T extends Prisma.User$profileArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$profileArgs<ExtArgs>>): Prisma.Prisma__ProfileClient<runtime.Types.Result.GetResult<Prisma.$ProfilePayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
||||
profiles<T extends Prisma.User$profilesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$profilesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ProfilePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
licenseKeys<T extends Prisma.User$licenseKeysArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$licenseKeysArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$LicenseKeyPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
userProclamations<T extends Prisma.User$userProclamationsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$userProclamationsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$UserProclamationsPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
/**
|
||||
@@ -2155,9 +2173,9 @@ export type User$accountsArgs<ExtArgs extends runtime.Types.Extensions.InternalA
|
||||
}
|
||||
|
||||
/**
|
||||
* User.profile
|
||||
* User.profiles
|
||||
*/
|
||||
export type User$profileArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
export type User$profilesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
/**
|
||||
* Select specific fields to fetch from the Profile
|
||||
*/
|
||||
@@ -2171,6 +2189,11 @@ export type User$profileArgs<ExtArgs extends runtime.Types.Extensions.InternalAr
|
||||
*/
|
||||
include?: Prisma.ProfileInclude<ExtArgs> | null
|
||||
where?: Prisma.ProfileWhereInput
|
||||
orderBy?: Prisma.ProfileOrderByWithRelationInput | Prisma.ProfileOrderByWithRelationInput[]
|
||||
cursor?: Prisma.ProfileWhereUniqueInput
|
||||
take?: number
|
||||
skip?: number
|
||||
distinct?: Prisma.ProfileScalarFieldEnum | Prisma.ProfileScalarFieldEnum[]
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,9 +5,9 @@ import {
|
||||
canonicalIdForFrequency,
|
||||
serializeCategory,
|
||||
serializeColorPalette,
|
||||
serializeGroup,
|
||||
serializeLicense,
|
||||
serializeProclamation,
|
||||
serializeSubscriptionLevel
|
||||
serializeProclamation
|
||||
} from './v2-serializers';
|
||||
|
||||
describe('canonicalIdForFrequency', () => {
|
||||
@@ -61,7 +61,7 @@ describe('serializeProclamation', () => {
|
||||
name: 'Peace & Relaxation',
|
||||
content: '<p>The text content, HTML formatted</p>',
|
||||
categoryId: 5,
|
||||
subscriptionLevelId: 1
|
||||
groupId: 1
|
||||
};
|
||||
|
||||
it('derives solfeggioFrequencyId from the Hz value, not the DB id', () => {
|
||||
@@ -84,13 +84,13 @@ describe('serializeProclamation', () => {
|
||||
expect(record).toBeNull();
|
||||
});
|
||||
|
||||
it('passes through a null subscriptionLevelId (kiosk treats as level 1)', () => {
|
||||
it('passes through categoryId and groupId unchanged', () => {
|
||||
const record = serializeProclamation({
|
||||
...base,
|
||||
subscriptionLevelId: null,
|
||||
solfeggioFrequency: solarPlexus528
|
||||
});
|
||||
expect(record?.subscriptionLevelId).toBeNull();
|
||||
expect(record?.categoryId).toBe(5);
|
||||
expect(record?.groupId).toBe(1);
|
||||
});
|
||||
|
||||
it('embeds a 7-color palette when provided', () => {
|
||||
@@ -116,64 +116,39 @@ describe('serializeProclamation', () => {
|
||||
});
|
||||
|
||||
describe('serializeCategory', () => {
|
||||
it('keeps parentId null for root categories', () => {
|
||||
expect(serializeCategory({ id: 4, name: 'Spiritual', sortOrder: 0, parentId: null })).toEqual({
|
||||
it('maps rows to a flat {id, name, sortOrder} with no parentId', () => {
|
||||
expect(serializeCategory({ id: 4, name: 'Spiritual', sortOrder: 0 })).toEqual({
|
||||
id: 4,
|
||||
name: 'Spiritual',
|
||||
sortOrder: 0,
|
||||
parentId: null
|
||||
sortOrder: 0
|
||||
});
|
||||
expect(
|
||||
Object.keys(serializeCategory({ id: 4, name: 'Spiritual', sortOrder: 0 }))
|
||||
).not.toContain('parentId');
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps parentId int for children', () => {
|
||||
expect(serializeCategory({ id: 5, name: 'General', sortOrder: 0, parentId: 4 })).toEqual({
|
||||
id: 5,
|
||||
describe('serializeGroup', () => {
|
||||
it('maps rows to {id, name, sortOrder}', () => {
|
||||
expect(serializeGroup({ id: 1, name: 'General', sortOrder: 0 })).toEqual({
|
||||
id: 1,
|
||||
name: 'General',
|
||||
sortOrder: 0,
|
||||
parentId: 4
|
||||
sortOrder: 0
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('serializeLicense', () => {
|
||||
it('returns subscription_level as an object when the level row exists', () => {
|
||||
it('returns only key and email_address (levels removed)', () => {
|
||||
const result = serializeLicense({
|
||||
key: 'XXXX-XXXX-XXXX-XXXX',
|
||||
emailAddress: 'user@example.com',
|
||||
levelName: 'Advanced',
|
||||
level: { id: 2, name: 'Advanced', description: 'General + Specific' }
|
||||
emailAddress: 'user@example.com'
|
||||
});
|
||||
expect(result).toEqual({
|
||||
key: 'XXXX-XXXX-XXXX-XXXX',
|
||||
email_address: 'user@example.com',
|
||||
subscription_level: { id: 2, name: 'Advanced', description: 'General + Specific' }
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to a structurally consistent object when no level row matches', () => {
|
||||
const result = serializeLicense({
|
||||
key: 'XXXX-XXXX-XXXX-XXXX',
|
||||
emailAddress: 'user@example.com',
|
||||
levelName: 'SomeCustomLevel',
|
||||
level: null
|
||||
});
|
||||
expect(result).toEqual({
|
||||
key: 'XXXX-XXXX-XXXX-XXXX',
|
||||
email_address: 'user@example.com',
|
||||
subscription_level: { id: null, name: 'SomeCustomLevel', description: null }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('serializeSubscriptionLevel', () => {
|
||||
it('maps rows to {id, name, description}', () => {
|
||||
expect(
|
||||
serializeSubscriptionLevel({ id: 1, name: 'Basic', description: 'General only' })
|
||||
).toEqual({
|
||||
id: 1,
|
||||
name: 'Basic',
|
||||
description: 'General only'
|
||||
email_address: 'user@example.com'
|
||||
});
|
||||
expect(Object.keys(result)).not.toContain('subscription_level');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -55,8 +55,8 @@ export type V2Proclamation = {
|
||||
name: string;
|
||||
content: string;
|
||||
categoryId: number;
|
||||
groupId: number;
|
||||
solfeggioFrequencyId: number;
|
||||
subscriptionLevelId: number | null;
|
||||
colorPalette?: string[];
|
||||
};
|
||||
|
||||
@@ -65,7 +65,7 @@ export type ProclamationWithFrequency = {
|
||||
name: string;
|
||||
content: string;
|
||||
categoryId: number;
|
||||
subscriptionLevelId: number | null;
|
||||
groupId: number;
|
||||
solfeggioFrequency: { value: number };
|
||||
};
|
||||
|
||||
@@ -83,8 +83,8 @@ export function serializeProclamation(
|
||||
name: p.name,
|
||||
content: p.content,
|
||||
categoryId: p.categoryId,
|
||||
solfeggioFrequencyId,
|
||||
subscriptionLevelId: p.subscriptionLevelId
|
||||
groupId: p.groupId,
|
||||
solfeggioFrequencyId
|
||||
};
|
||||
|
||||
if (palette && palette.length > 0) {
|
||||
@@ -98,53 +98,32 @@ export type V2Category = {
|
||||
id: number;
|
||||
name: string;
|
||||
sortOrder: number;
|
||||
parentId: number | null;
|
||||
};
|
||||
|
||||
export function serializeCategory(c: {
|
||||
export function serializeCategory(c: { id: number; name: string; sortOrder: number }): V2Category {
|
||||
return { id: c.id, name: c.name, sortOrder: c.sortOrder };
|
||||
}
|
||||
|
||||
export type V2Group = {
|
||||
id: number;
|
||||
name: string;
|
||||
sortOrder: number;
|
||||
parentId: number | null;
|
||||
}): V2Category {
|
||||
return { id: c.id, name: c.name, sortOrder: c.sortOrder, parentId: c.parentId };
|
||||
}
|
||||
|
||||
export type V2SubscriptionLevel = {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string | null;
|
||||
};
|
||||
|
||||
export function serializeSubscriptionLevel(level: {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string | null;
|
||||
}): V2SubscriptionLevel {
|
||||
return { id: level.id, name: level.name, description: level.description };
|
||||
export function serializeGroup(g: { id: number; name: string; sortOrder: number }): V2Group {
|
||||
return { id: g.id, name: g.name, sortOrder: g.sortOrder };
|
||||
}
|
||||
|
||||
export type V2License = {
|
||||
key: string;
|
||||
email_address: string;
|
||||
subscription_level: { id: number | null; name: string; description: string | null };
|
||||
};
|
||||
|
||||
/** Build the POST /license response. `subscription_level` is always an object;
|
||||
* when no SubscriptionLevel row matches the license's level name, fall back to
|
||||
* a structurally consistent object with a null id/description. */
|
||||
export function serializeLicense(opts: {
|
||||
key: string;
|
||||
emailAddress: string;
|
||||
levelName: string;
|
||||
level: { id: number; name: string; description: string | null } | null;
|
||||
}): V2License {
|
||||
/** Build the POST /license response. */
|
||||
export function serializeLicense(opts: { key: string; emailAddress: string }): V2License {
|
||||
return {
|
||||
key: opts.key,
|
||||
email_address: opts.emailAddress,
|
||||
subscription_level: opts.level
|
||||
? { id: opts.level.id, name: opts.level.name, description: opts.level.description }
|
||||
: { id: null, name: opts.levelName, description: null }
|
||||
email_address: opts.emailAddress
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ function signedRequest(opts: {
|
||||
const path = '/api/v2/automations/create/account';
|
||||
const timestamp = String(Math.floor((opts.now ?? new Date()).getTime() / 1000));
|
||||
const nonce = 'a48c0f2e-5f34-4b0b-9a12-3c7d6b8e1f00';
|
||||
const body = '{"email":"user@example.com","subscription_level":"Basic"}';
|
||||
const body = '{"email":"user@example.com"}';
|
||||
|
||||
let ts = timestamp;
|
||||
let nonceValue = nonce;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
export function superFormErrorsToFieldErrors(superErrors: Record<string, unknown>): {
|
||||
error: string;
|
||||
fieldErrors: Record<string, string>;
|
||||
} {
|
||||
const fieldErrors: Record<string, string> = {};
|
||||
let formError = '';
|
||||
for (const [key, value] of Object.entries(superErrors)) {
|
||||
if (key === '_errors' && Array.isArray(value)) {
|
||||
formError = (value[0] as string) ?? '';
|
||||
} else if (
|
||||
value &&
|
||||
typeof value === 'object' &&
|
||||
'_errors' in value &&
|
||||
Array.isArray((value as Record<string, unknown>)._errors)
|
||||
) {
|
||||
const errMsg = ((value as Record<string, unknown>)._errors as string[])[0];
|
||||
fieldErrors[key] = errMsg;
|
||||
} else if (Array.isArray(value) && typeof value[0] === 'string') {
|
||||
fieldErrors[key] = value[0];
|
||||
}
|
||||
}
|
||||
return { error: formError, fieldErrors };
|
||||
}
|
||||
@@ -1,391 +0,0 @@
|
||||
# svelte-admin
|
||||
|
||||
A config-driven admin panel framework for SvelteKit + Prisma. Define your models once in a
|
||||
registry and get full CRUD UI with per-model static pages, role-based access control, searchable
|
||||
tables, FK dropdowns, TipTap editor for rich text, and related-record panels.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/lib/svelte-admin/ # Framework core
|
||||
├── types.ts # AdminModelConfig and related types
|
||||
├── registry.ts # Model configurations (the single source of truth)
|
||||
├── roles.ts # Role hierarchy and access helpers
|
||||
├── prisma-delegate.ts # Dynamic Prisma delegate lookup
|
||||
├── introspect.ts # Field label generation and dot-notation resolution
|
||||
├── form-data.ts # FormData -> typed object parser
|
||||
├── crud.ts # CRUD action factory (load, upsert, delete)
|
||||
└── index.ts # Barrel exports
|
||||
|
||||
src/routes/(protected)/admin/ # Per-model admin pages (no dynamic [model] routing)
|
||||
├── +layout.server.ts # Role-based access guard
|
||||
├── +layout.svelte # Layout shell
|
||||
├── +page.server.ts # Admin home (model grid)
|
||||
├── +page.svelte # Admin home UI
|
||||
├── users/ # Each model gets its own page directory
|
||||
├── profiles/
|
||||
├── addresses/
|
||||
├── user-roles/
|
||||
├── proclamations/
|
||||
├── solfeggio-frequencies/
|
||||
├── proclamation-categories/
|
||||
├── subscription-levels/
|
||||
└── license-keys/ # listOnly — no create/edit modal
|
||||
|
||||
src/lib/components/admin/ # Reusable admin UI components
|
||||
├── AdminTable.svelte # Sortable, searchable data table
|
||||
├── AdminForm.svelte # Dynamic form with type-aware inputs
|
||||
├── AdminModal.svelte # Modal overlay for CRUD forms
|
||||
├── TipTap.svelte # Rich-text editor (in components/tiptap/)
|
||||
```
|
||||
|
||||
## Adding a New Model
|
||||
|
||||
1. **Define the Prisma model** in `prisma/schema.prisma` and create a migration:
|
||||
|
||||
```bash
|
||||
npm run migrate -- --name add_my_model
|
||||
```
|
||||
|
||||
2. **Add a config object** to the `models` array in `registry.ts`:
|
||||
|
||||
```ts
|
||||
{
|
||||
slug: 'widgets', // URL segment: /admin/widgets
|
||||
modelName: 'Widget', // Prisma model name (PascalCase, singular)
|
||||
displayName: 'Widgets', // Plural display name
|
||||
displayNameSingular: 'Widget', // Singular display name
|
||||
idType: 'String', // 'String' or 'Int' — matches Prisma @id type
|
||||
include: { category: true }, // Optional: Prisma include for relations
|
||||
listFields: ['name', 'category.name', 'price'],
|
||||
searchFields: ['name'],
|
||||
formFields: ['name', 'categoryId', 'price'],
|
||||
readonlyFields: ['id', 'createdAt'],
|
||||
fieldTypes: {
|
||||
name: 'String',
|
||||
categoryId: 'Int',
|
||||
price: 'Float',
|
||||
createdAt: 'DateTime'
|
||||
},
|
||||
relationOptions: {
|
||||
categoryId: {
|
||||
model: 'Category',
|
||||
labelField: 'name',
|
||||
valueField: 'id'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Create the page directory** `src/routes/(protected)/admin/widgets/` with:
|
||||
- `+page.server.ts` — load records + form actions using `crud.ts` helpers
|
||||
- `+page.svelte` — render AdminTable + AdminModal with AdminForm
|
||||
|
||||
4. **Done.** Visit `/admin/widgets`. The page handles list, create, edit via modal.
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### AdminModelConfig
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| --------------------- | ---------------------------------------------- | -------- | ------------------------------------------------------------------------------------------ |
|
||||
| `slug` | `string` | Yes | URL segment (kebab-case). Used in `/admin/{slug}`. |
|
||||
| `modelName` | `string` | Yes | Prisma model name (PascalCase, **singular**). Maps to `prisma[lcfirst(modelName)]`. |
|
||||
| `displayName` | `string` | Yes | Plural display name for headings and navigation. |
|
||||
| `displayNameSingular` | `string` | Yes | Singular name for edit page headings. |
|
||||
| `idType` | `'String' \| 'Int'` | Yes | Primary key type. Controls how the `id` URL param is parsed. |
|
||||
| `include` | `Record<string, boolean \| { include: ... }>` | No | Prisma `include` clause for eager-loading relations. |
|
||||
| `listFields` | `string[]` | Yes | Table columns. Supports dot-notation for relations (e.g., `'role.name'`). |
|
||||
| `searchFields` | `string[]` | Yes | Fields searched by the client-side search filter. |
|
||||
| `formFields` | `string[]` | Yes | Fields rendered in create/edit forms, in display order. |
|
||||
| `readonlyFields` | `string[]` | No | Fields shown as read-only on the edit page. |
|
||||
| `listOnly` | `boolean` | No | If `true`, suppresses edit links in the table and no modal. |
|
||||
| `fieldTypes` | `Record<string, PrismaScalarType>` | Yes | Maps field names to scalar types. Controls input rendering and FormData parsing. |
|
||||
| `relationOptions` | `Record<string, RelationOptionConfig>` | No | FK fields that render as `<select>` dropdowns. |
|
||||
| `relatedRecords` | `RelatedRecordConfig[]` | No | Has-one/has-many panels shown on edit pages (display only via related records table). |
|
||||
| `accessFilter` | `(viewerRoleId: number) => where \| undefined` | No | Returns a Prisma `where` clause to filter records by viewer role. `undefined` = no filter. |
|
||||
|
||||
### PrismaScalarType
|
||||
|
||||
`'String' | 'Int' | 'Float' | 'Boolean' | 'DateTime' | 'Tiptap'`
|
||||
|
||||
Controls both form input rendering and FormData parsing:
|
||||
|
||||
| Type | HTML Input | Parse Logic |
|
||||
| ---------- | -------------------------------------- | ------------------------------- |
|
||||
| `String` | `<input type="text">` | `raw.toString()` |
|
||||
| `Int` | `<input type="number" step="1">` | `parseInt()`, empty -> `null` |
|
||||
| `Float` | `<input type="number" step="0.01">` | `parseFloat()`, empty -> `null` |
|
||||
| `Boolean` | `<input type="checkbox">` | `'on'` or `'true'` -> `true` |
|
||||
| `DateTime` | `<input type="datetime-local">` | `new Date()`, empty -> `null` |
|
||||
| `Tiptap` | TipTap rich-text editor (hidden input) | `raw.toString()` |
|
||||
|
||||
### RelationOptionConfig
|
||||
|
||||
```ts
|
||||
{
|
||||
model: 'UserRole', // Prisma model to query for options
|
||||
labelField: 'name', // Field displayed in the dropdown
|
||||
valueField: 'id' // Field value submitted with the form
|
||||
}
|
||||
```
|
||||
|
||||
### RelatedRecordConfig
|
||||
|
||||
```ts
|
||||
{
|
||||
relationKey: 'profile', // Key on parent record (must match a key in `include`)
|
||||
label: 'Profile', // Section heading on edit page
|
||||
type: 'has-one', // 'has-one' or 'has-many'
|
||||
displayFields: ['firstName', 'lastName'], // Fields shown in the panel/table
|
||||
editSlug: 'profiles' // Optional: admin slug to link for editing
|
||||
}
|
||||
```
|
||||
|
||||
## Field Types & Form Rendering
|
||||
|
||||
The `fieldTypes` mapping is the central mechanism connecting Prisma scalar types to HTML inputs
|
||||
and form data parsing. Every field in `formFields` must have a corresponding entry in `fieldTypes`.
|
||||
|
||||
When a field also has a `relationOptions` entry, the framework renders a `<select>` dropdown
|
||||
instead of the default input for that type. The options are fetched server-side by `crud.ts`
|
||||
and passed to the client as `relationOptionsData`.
|
||||
|
||||
For `Tiptap` fields, the TipTap rich-text editor is rendered. The editor content is stored in a
|
||||
hidden input and submitted with the form.
|
||||
|
||||
## Relations
|
||||
|
||||
### Foreign Key Dropdowns (relationOptions)
|
||||
|
||||
When a form field is a foreign key, add a `relationOptions` entry to render a `<select>` dropdown:
|
||||
|
||||
```ts
|
||||
{
|
||||
formFields: ['name', 'categoryId'],
|
||||
fieldTypes: { name: 'String', categoryId: 'Int' },
|
||||
relationOptions: {
|
||||
categoryId: {
|
||||
model: 'ProclamationCategory',
|
||||
labelField: 'name',
|
||||
valueField: 'id'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `crud.ts` `loadRecords` function automatically queries the related model and passes options
|
||||
to the page as `relationOptionsData`.
|
||||
|
||||
### Eager Loading (include)
|
||||
|
||||
Use `include` to load related data for display in list tables or edit pages:
|
||||
|
||||
```ts
|
||||
{
|
||||
include: { category: { include: { parent: true } } },
|
||||
listFields: ['name', 'category.name'] // dot-notation resolves nested values
|
||||
}
|
||||
```
|
||||
|
||||
## Related Records
|
||||
|
||||
Display has-one or has-many related records as panels on the edit page using `relatedRecords`:
|
||||
|
||||
```ts
|
||||
relatedRecords: [
|
||||
{
|
||||
relationKey: 'profile',
|
||||
label: 'Profile',
|
||||
type: 'has-one',
|
||||
displayFields: ['firstName', 'lastName'],
|
||||
editSlug: 'profiles' // links to /admin/profiles/{id}
|
||||
},
|
||||
{
|
||||
relationKey: 'licenseKeys',
|
||||
label: 'License Keys',
|
||||
type: 'has-many',
|
||||
displayFields: ['key', 'active', 'level']
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
The `relationKey` must match a key in `include` so the data is actually loaded.
|
||||
|
||||
## Access Control
|
||||
|
||||
### Role Hierarchy
|
||||
|
||||
Defined in `roles.ts`. Lower numbers = higher privilege:
|
||||
|
||||
| Constant | Value | Access Level |
|
||||
| ------------- | ----- | -------------------------------------- |
|
||||
| `ROLES.SUPER` | 1 | Full access, sees all users |
|
||||
| `ROLES.ADMIN` | 2 | Admin access, sees staff and below |
|
||||
| `ROLES.STAFF` | 3 | Staff access (minimum for admin panel) |
|
||||
| `ROLES.USER` | 4 | Regular user (no admin access) |
|
||||
|
||||
### Layout Guard
|
||||
|
||||
`+layout.server.ts` calls `canAccessAdmin(user.roleId)` which returns `true` when
|
||||
`roleId <= ROLES.STAFF`. Users with `ROLES.USER` (4) or no role get a 403.
|
||||
|
||||
### Per-Model Access Filtering
|
||||
|
||||
Use `accessFilter` to restrict which records a viewer can see:
|
||||
|
||||
```ts
|
||||
accessFilter: (viewerRoleId: number) => {
|
||||
if (viewerRoleId <= ROLES.SUPER) return undefined;
|
||||
if (viewerRoleId <= ROLES.ADMIN) {
|
||||
return { roleId: { gte: ROLES.STAFF } };
|
||||
}
|
||||
return { roleId: { gt: ROLES.STAFF } };
|
||||
};
|
||||
```
|
||||
|
||||
## How Static Pages Work
|
||||
|
||||
1. User visits `/admin/proclamations`
|
||||
2. SvelteKit matches the static route `/admin/proclamations/+page.server.ts`
|
||||
3. `+page.server.ts` calls `getModelConfig('proclamations')` to look up the config
|
||||
4. `loadRecords(config)` calls `prisma.proclamation.findMany()` with the config's `include` and `accessFilter`
|
||||
5. For each FK field with a `relationOptions`, it also queries the related model for dropdown options
|
||||
6. Data + config are passed to `+page.svelte`
|
||||
7. The page renders `AdminTable` with records and `AdminModal` + `AdminForm` for create/edit
|
||||
|
||||
## Component API
|
||||
|
||||
### AdminTable
|
||||
|
||||
| Prop | Type | Description |
|
||||
| -------------- | ---------------------------------- | ------------------------------------ |
|
||||
| `records` | `Record<string, unknown>[]` | Data rows |
|
||||
| `listFields` | `string[]` | Column keys (dot-notation supported) |
|
||||
| `searchFields` | `string[]` | Fields for client-side search |
|
||||
| `fieldTypes` | `Record<string, PrismaScalarType>` | Type mapping for cell formatting |
|
||||
| `singular` | `string` | Singular label for UI text |
|
||||
| `onRowClick` | `(record) => void?` | Click handler for edit modal |
|
||||
| `onNew` | `() => void?` | Click handler for create modal |
|
||||
|
||||
Features: client-side search, click-to-sort columns, auto-formatted cells (dates, booleans,
|
||||
decimals), empty state.
|
||||
|
||||
### AdminForm
|
||||
|
||||
| Prop | Type | Description |
|
||||
| --------------------- | --------------------------------------- | -------------------------------- |
|
||||
| `fields` | `string[]` | Field names to render |
|
||||
| `fieldTypes` | `Record<string, PrismaScalarType>` | Type mapping for inputs |
|
||||
| `relationOptions` | `Record<string, RelationOptionConfig>?` | FK dropdown configs |
|
||||
| `relationOptionsData` | `Record<string, { label; value }[]>?` | Loaded dropdown options |
|
||||
| `record` | `Record<string, unknown>?` | Pre-fill values (edit mode) |
|
||||
| `readonlyFields` | `string[]?` | Read-only field names |
|
||||
| `action` | `string` | Form action (e.g., `'?/upsert'`) |
|
||||
| `submitLabel` | `string?` | Button text |
|
||||
|
||||
### AdminModal
|
||||
|
||||
| Prop | Type | Description |
|
||||
| ---------- | ------------ | ------------------ |
|
||||
| `open` | `boolean` | Show/hide modal |
|
||||
| `title` | `string` | Modal heading |
|
||||
| `onClose` | `() => void` | Close handler |
|
||||
| `children` | `Snippet` | Modal body content |
|
||||
|
||||
### TipTap
|
||||
|
||||
| Prop | Type | Description |
|
||||
| ---------------- | -------- | --------------------------------- |
|
||||
| `initialContent` | `string` | Initial HTML content |
|
||||
| `name` | `string` | Hidden input name for form submit |
|
||||
|
||||
## Utility Functions
|
||||
|
||||
| Function | Module | Description |
|
||||
| ------------------------------------------ | -------------------- | ------------------------------------------------------------------------------ |
|
||||
| `fieldNameToLabel(name)` | `introspect.ts` | `'solfeggioFrequencyId'` -> `'Solfeggio Frequency'` (strips `Id`, adds spaces) |
|
||||
| `resolveFieldValue(record, path)` | `introspect.ts` | Resolves dot-notation paths like `'category.name'` on nested objects |
|
||||
| `extractFormData(formData, fields, types)` | `form-data.ts` | Parses `FormData` with type-aware conversion per `PrismaScalarType` |
|
||||
| `getDelegate(config)` | `prisma-delegate.ts` | Returns `prisma[lcfirst(modelName)]` delegate |
|
||||
| `parseId(config, raw)` | `prisma-delegate.ts` | Parses string to `number` when `idType` is `'Int'` |
|
||||
| `getModelConfig(slug)` | `registry.ts` | Looks up config by URL slug |
|
||||
| `getAllModelConfigs()` | `registry.ts` | Returns all registered model configs |
|
||||
| `hasRole(roleId, required)` | `roles.ts` | `true` if `roleId <= required` |
|
||||
| `canAccessAdmin(roleId)` | `roles.ts` | `true` if `roleId <= ROLES.STAFF` |
|
||||
| `loadRecords(locals, config)` | `crud.ts` | Fetches records + FK dropdown options for a model |
|
||||
| `upsertRecord(formData, config, locals)` | `crud.ts` | Creates or updates a record based on presence of `id` in form data |
|
||||
| `deleteRecord(formData, config, locals)` | `crud.ts` | Deletes a record by ID |
|
||||
|
||||
## Models in Admin
|
||||
|
||||
| Registry Slug | Prisma Model | Notes |
|
||||
| ------------------------- | -------------------- | ----------------------------------- |
|
||||
| `users` | User | Role assignment, view-only fields |
|
||||
| `profiles` | Profile | Linked to user FK |
|
||||
| `addresses` | Address | Linked to profile FK |
|
||||
| `user-roles` | UserRole | |
|
||||
| `proclamations` | Proclamation | TipTap rich-text editor for content |
|
||||
| `solfeggio-frequencies` | SolfeggioFrequency | |
|
||||
| `proclamation-categories` | ProclamationCategory | Parent category hierarchy |
|
||||
| `license-keys` | LicenseKey | listOnly — read-only table, no CRUD |
|
||||
| `subscription-levels` | SubscriptionLevel | |
|
||||
|
||||
### Excluded from Admin
|
||||
|
||||
| Model | Reason |
|
||||
| ----------------- | --------------------------------------------- |
|
||||
| Session | Better Auth internal — not for manual editing |
|
||||
| Account | Better Auth internal |
|
||||
| Verification | Better Auth internal |
|
||||
| UserProclamations | Composite key — managed via other models |
|
||||
|
||||
## Complete Example
|
||||
|
||||
A hypothetical `Product` model showing all configuration features:
|
||||
|
||||
```ts
|
||||
{
|
||||
slug: 'products',
|
||||
modelName: 'Product',
|
||||
displayName: 'Products',
|
||||
displayNameSingular: 'Product',
|
||||
idType: 'Int',
|
||||
include: { category: true, variants: true, createdBy: true },
|
||||
listFields: ['name', 'category.name', 'price', 'active', 'createdAt'],
|
||||
searchFields: ['name', 'sku'],
|
||||
formFields: ['name', 'sku', 'price', 'active', 'categoryId'],
|
||||
readonlyFields: ['id', 'createdAt', 'updatedAt'],
|
||||
fieldTypes: {
|
||||
name: 'String',
|
||||
sku: 'String',
|
||||
price: 'Float',
|
||||
active: 'Boolean',
|
||||
categoryId: 'Int',
|
||||
createdAt: 'DateTime',
|
||||
updatedAt: 'DateTime'
|
||||
},
|
||||
relationOptions: {
|
||||
categoryId: { model: 'Category', labelField: 'name', valueField: 'id' }
|
||||
},
|
||||
relatedRecords: [
|
||||
{
|
||||
relationKey: 'variants',
|
||||
label: 'Variants',
|
||||
type: 'has-many',
|
||||
displayFields: ['size', 'color', 'stock'],
|
||||
editSlug: 'variants'
|
||||
},
|
||||
{
|
||||
relationKey: 'createdBy',
|
||||
label: 'Created By',
|
||||
type: 'has-one',
|
||||
displayFields: ['name', 'email']
|
||||
}
|
||||
],
|
||||
accessFilter: (viewerRoleId: number) => {
|
||||
if (viewerRoleId <= 2) return undefined;
|
||||
return { active: true };
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,227 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createLocals, mockUser } from '$lib/test-utils';
|
||||
import { getModelConfig } from './registry';
|
||||
|
||||
// Ensure superValidate can be imported from server path in Node.js/SSR context
|
||||
import { superValidate } from 'sveltekit-superforms/server';
|
||||
import { zod4 } from 'sveltekit-superforms/adapters';
|
||||
|
||||
vi.mock('$lib/db', () => {
|
||||
const mkdel = () => ({
|
||||
findMany: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
findUnique: vi.fn(),
|
||||
findFirst: vi.fn()
|
||||
});
|
||||
return {
|
||||
prisma: {
|
||||
userRole: mkdel(),
|
||||
proclamation: mkdel(),
|
||||
proclamationCategory: mkdel(),
|
||||
solfeggioFrequency: mkdel(),
|
||||
subscriptionLevel: mkdel()
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
import { prisma } from '$lib/db';
|
||||
import { loadRecords, upsertRecord, deleteRecord } from './crud';
|
||||
|
||||
const userRoleFindMany = prisma.userRole.findMany as ReturnType<typeof vi.fn>;
|
||||
const create = prisma.userRole.create as ReturnType<typeof vi.fn>;
|
||||
const update = prisma.userRole.update as ReturnType<typeof vi.fn>;
|
||||
const deletes = prisma.userRole.delete as ReturnType<typeof vi.fn>;
|
||||
const findUnique = prisma.userRole.findUnique as ReturnType<typeof vi.fn>;
|
||||
const findFirst = prisma.userRole.findFirst as ReturnType<typeof vi.fn>;
|
||||
const config = getModelConfig('user-roles')!;
|
||||
|
||||
describe('crud loadRecords', () => {
|
||||
beforeEach(() => {
|
||||
userRoleFindMany.mockReset();
|
||||
});
|
||||
|
||||
it('returns records', async () => {
|
||||
userRoleFindMany.mockResolvedValue([{ id: 1, name: 'Admin' }]);
|
||||
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const result = await loadRecords(locals, config);
|
||||
|
||||
expect(result.records).toEqual([{ id: 1, name: 'Admin' }]);
|
||||
});
|
||||
|
||||
it('returns empty records array', async () => {
|
||||
userRoleFindMany.mockResolvedValue([]);
|
||||
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const result = await loadRecords(locals, config);
|
||||
|
||||
expect(result.records).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('crud upsertRecord', () => {
|
||||
beforeEach(() => {
|
||||
create.mockReset();
|
||||
update.mockReset();
|
||||
findUnique.mockReset();
|
||||
findFirst.mockReset();
|
||||
});
|
||||
|
||||
it('creates a new record when no id', async () => {
|
||||
create.mockResolvedValue({ id: 2, name: 'Editor' });
|
||||
|
||||
const fd = new FormData();
|
||||
fd.set('name', 'Editor');
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const result = await upsertRecord(fd, config, locals);
|
||||
|
||||
expect(create).toHaveBeenCalledWith({ data: { name: 'Editor' } });
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
|
||||
it('updates an existing record when id is present', async () => {
|
||||
findUnique.mockResolvedValue({ id: 1, name: 'Old Name' });
|
||||
|
||||
const fd = new FormData();
|
||||
fd.set('id', '1');
|
||||
fd.set('name', 'New Name');
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const result = await upsertRecord(fd, config, locals);
|
||||
|
||||
expect(update).toHaveBeenCalledWith({ where: { id: 1 }, data: { name: 'New Name' } });
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
|
||||
it('returns error when updating non-existent record', async () => {
|
||||
findUnique.mockResolvedValue(null);
|
||||
|
||||
const fd = new FormData();
|
||||
fd.set('id', '999');
|
||||
fd.set('name', 'Ghost');
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const result = await upsertRecord(fd, config, locals);
|
||||
|
||||
expect(result).toEqual({ success: false, error: 'Record not found' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('crud deleteRecord', () => {
|
||||
beforeEach(() => {
|
||||
deletes.mockReset();
|
||||
});
|
||||
|
||||
it('deletes a record by id', async () => {
|
||||
deletes.mockResolvedValue({ id: 1, name: 'Admin' });
|
||||
|
||||
const fd = new FormData();
|
||||
fd.set('id', '1');
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const result = await deleteRecord(fd, config, locals);
|
||||
|
||||
expect(deletes).toHaveBeenCalledWith({ where: { id: 1 } });
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
|
||||
it('returns error when id is missing', async () => {
|
||||
const fd = new FormData();
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const result = await deleteRecord(fd, config, locals);
|
||||
|
||||
expect(result).toEqual({ success: false, error: 'Missing id' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('P2002 unique constraint handling', () => {
|
||||
beforeEach(() => {
|
||||
create.mockReset();
|
||||
});
|
||||
|
||||
it('returns error with field names when meta.target is populated', async () => {
|
||||
const p2002 = new Error('Unique constraint failed');
|
||||
(p2002 as any).code = 'P2002';
|
||||
(p2002 as any).meta = { target: ['name'], modelName: 'UserRole' };
|
||||
create.mockRejectedValue(p2002);
|
||||
|
||||
const fd = new FormData();
|
||||
fd.set('name', 'Admin');
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const result = await upsertRecord(fd, config, locals);
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
error: 'A record with this name already exists.',
|
||||
fieldErrors: { name: 'Already taken' }
|
||||
});
|
||||
});
|
||||
|
||||
it('returns generic message when meta.target is empty', async () => {
|
||||
const p2002 = new Error('Unique constraint failed');
|
||||
(p2002 as any).code = 'P2002';
|
||||
(p2002 as any).meta = { target: [], modelName: 'UserRole' };
|
||||
create.mockRejectedValue(p2002);
|
||||
|
||||
const fd = new FormData();
|
||||
fd.set('name', 'Admin');
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const result = await upsertRecord(fd, config, locals);
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
error: 'A database constraint violation occurred. The record could not be created.'
|
||||
});
|
||||
});
|
||||
|
||||
it('returns generic message when meta.target is missing', async () => {
|
||||
const p2002 = new Error('Unique constraint failed');
|
||||
(p2002 as any).code = 'P2002';
|
||||
(p2002 as any).meta = { modelName: 'UserRole' };
|
||||
create.mockRejectedValue(p2002);
|
||||
|
||||
const fd = new FormData();
|
||||
fd.set('name', 'Admin');
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const result = await upsertRecord(fd, config, locals);
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
error: 'A database constraint violation occurred. The record could not be created.'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sveltekit-superforms import compatibility', () => {
|
||||
it('imports superValidate from server path without pulling in SuperDebug.svelte', async () => {
|
||||
expect(superValidate).toBeDefined();
|
||||
});
|
||||
|
||||
it('calls superValidate with a Request and validates against a Zod schema', async () => {
|
||||
const { z } = await import('zod');
|
||||
const schema = z.object({ name: z.string().min(1) });
|
||||
const body = new URLSearchParams({ name: 'test' });
|
||||
const request = new Request('http://localhost', {
|
||||
method: 'POST',
|
||||
body,
|
||||
headers: { 'content-type': 'application/x-www-form-urlencoded' }
|
||||
});
|
||||
const form = await superValidate(request, zod4(schema));
|
||||
expect(form.valid).toBe(true);
|
||||
expect(form.data).toEqual({ name: 'test' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('prisma client version consistency', () => {
|
||||
it('generated client references existing query compiler files', async () => {
|
||||
const fs = await import('fs');
|
||||
const path = await import('path');
|
||||
const runtimeDir = path.resolve('node_modules/@prisma/client/runtime');
|
||||
|
||||
const generatedFile = path.resolve('src/lib/generated/prisma/internal/class.ts');
|
||||
const content = fs.readFileSync(generatedFile, 'utf-8');
|
||||
const compilerMatch = content.match(/import\(["']@prisma\/client\/runtime\/([^"']+)["']\)/);
|
||||
expect(compilerMatch).not.toBeNull();
|
||||
const compilerPath = path.join(runtimeDir, compilerMatch![1]);
|
||||
expect(fs.existsSync(compilerPath)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,190 +0,0 @@
|
||||
import { getDelegate, parseId } from './prisma-delegate';
|
||||
import { extractFormData } from './form-data';
|
||||
import type { AdminModelConfig } from './types';
|
||||
import { superValidate } from 'sveltekit-superforms/server';
|
||||
import { zod4 } from 'sveltekit-superforms/adapters';
|
||||
import * as Schemas from './schemas';
|
||||
import type { z } from 'zod';
|
||||
|
||||
const modelSchemas: Record<string, z.ZodObject<any>> = {
|
||||
User: Schemas.User,
|
||||
Profile: Schemas.Profile,
|
||||
UserRole: Schemas.UserRole,
|
||||
Proclamation: Schemas.Proclamation,
|
||||
ProclamationCategory: Schemas.ProclamationCategory,
|
||||
SolfeggioFrequency: Schemas.SolfeggioFrequency,
|
||||
SubscriptionLevel: Schemas.SubscriptionLevel,
|
||||
LicenseKey: Schemas.LicenseKey
|
||||
};
|
||||
|
||||
export function superFormErrorsToFieldErrors(superErrors: Record<string, unknown>): {
|
||||
error: string;
|
||||
fieldErrors: Record<string, string>;
|
||||
} {
|
||||
const fieldErrors: Record<string, string> = {};
|
||||
let formError = '';
|
||||
for (const [key, value] of Object.entries(superErrors)) {
|
||||
if (key === '_errors' && Array.isArray(value)) {
|
||||
formError = (value[0] as string) ?? '';
|
||||
} else if (
|
||||
value &&
|
||||
typeof value === 'object' &&
|
||||
'_errors' in value &&
|
||||
Array.isArray((value as Record<string, unknown>)._errors)
|
||||
) {
|
||||
const errMsg = ((value as Record<string, unknown>)._errors as string[])[0];
|
||||
fieldErrors[key] = errMsg;
|
||||
}
|
||||
}
|
||||
return { error: formError, fieldErrors };
|
||||
}
|
||||
|
||||
export async function loadRecords(locals: App.Locals, config: AdminModelConfig) {
|
||||
const delegate = getDelegate(config);
|
||||
|
||||
const viewerRoleId = (locals.user as { roleId?: number } | null)?.roleId ?? 4;
|
||||
const where = config.accessFilter?.(viewerRoleId);
|
||||
|
||||
const records = await delegate.findMany({
|
||||
where,
|
||||
include: config.include,
|
||||
orderBy: { id: 'asc' }
|
||||
});
|
||||
|
||||
const relationOptionsData: Record<string, { label: string; value: string | number }[]> = {};
|
||||
|
||||
for (const [field, relConfig] of Object.entries(config.relationOptions ?? {})) {
|
||||
const relDelegate = getDelegate({ ...config, modelName: relConfig.model });
|
||||
const selectFields: Record<string, boolean> = {
|
||||
[relConfig.valueField]: true,
|
||||
[relConfig.labelField]: true
|
||||
};
|
||||
const options = await relDelegate.findMany({
|
||||
select: selectFields,
|
||||
orderBy: { [relConfig.labelField]: 'asc' }
|
||||
});
|
||||
const mapped = (options as Record<string, unknown>[]).map((opt: Record<string, unknown>) => ({
|
||||
label: String(opt[relConfig.labelField]),
|
||||
value: opt[relConfig.valueField] as string | number
|
||||
}));
|
||||
relationOptionsData[field] = mapped;
|
||||
}
|
||||
|
||||
return {
|
||||
records: records as Record<string, unknown>[],
|
||||
relationOptionsData
|
||||
};
|
||||
}
|
||||
|
||||
export async function upsertRecord(
|
||||
requestOrFormData: Request | FormData,
|
||||
config: AdminModelConfig,
|
||||
locals: App.Locals
|
||||
) {
|
||||
const formData =
|
||||
requestOrFormData instanceof FormData ? requestOrFormData : await requestOrFormData.formData();
|
||||
|
||||
const schema = modelSchemas[config.modelName];
|
||||
const form = schema ? await superValidate(formData, zod4(schema)) : null;
|
||||
|
||||
if (form && !form.valid) {
|
||||
const { error, fieldErrors } = superFormErrorsToFieldErrors(
|
||||
form.errors as Record<string, unknown>
|
||||
);
|
||||
return { ...form, success: false, error, fieldErrors };
|
||||
}
|
||||
|
||||
const rawId = formData.get('id')?.toString();
|
||||
const data =
|
||||
form?.data ??
|
||||
(extractFormData(formData, config.formFields, config.fieldTypes) as Record<string, unknown>);
|
||||
|
||||
// Strip virtual/UI-only fields from Prisma payload
|
||||
for (const f of config.dataOnlyFields ?? []) {
|
||||
delete data[f];
|
||||
}
|
||||
|
||||
const delegate = getDelegate(config);
|
||||
const viewerRoleId = (locals.user as { roleId?: number } | null)?.roleId ?? 4;
|
||||
const where = config.accessFilter?.(viewerRoleId);
|
||||
|
||||
if (rawId) {
|
||||
const id = parseId(config, rawId);
|
||||
|
||||
const existing = await delegate.findUnique({
|
||||
where: { id },
|
||||
...((config.include ? { include: config.include } : {}) as object)
|
||||
});
|
||||
if (!existing) {
|
||||
return { success: false, error: 'Record not found' };
|
||||
}
|
||||
|
||||
if (where) {
|
||||
const match = await delegate.findFirst({
|
||||
where: { id, ...where }
|
||||
});
|
||||
if (!match) {
|
||||
return { success: false, error: 'Access denied' };
|
||||
}
|
||||
}
|
||||
|
||||
await delegate.update({
|
||||
where: { id },
|
||||
data
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} else {
|
||||
try {
|
||||
await delegate.create({ data });
|
||||
} catch (e: unknown) {
|
||||
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2002') {
|
||||
const meta = (e as { meta?: { target?: string[]; modelName?: string } }).meta;
|
||||
const fields = meta?.target ?? [];
|
||||
console.error(
|
||||
`[crud] P2002 on ${meta?.modelName ?? 'unknown'} create. data=`,
|
||||
data,
|
||||
`meta.target=`,
|
||||
meta?.target
|
||||
);
|
||||
if (fields.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'A database constraint violation occurred. The record could not be created.'
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: `A record with this ${fields.join(', ')} already exists.`,
|
||||
fieldErrors: Object.fromEntries(fields.map((f) => [f, 'Already taken']))
|
||||
};
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteRecord(
|
||||
requestOrFormData: Request | FormData,
|
||||
config: AdminModelConfig,
|
||||
locals: App.Locals
|
||||
) {
|
||||
const delegate = getDelegate(config);
|
||||
const formData =
|
||||
requestOrFormData instanceof FormData ? requestOrFormData : await requestOrFormData.formData();
|
||||
const rawId = formData.get('id')?.toString();
|
||||
if (!rawId) return { success: false, error: 'Missing id' };
|
||||
|
||||
const id = parseId(config, rawId);
|
||||
|
||||
const viewerRoleId = (locals.user as { roleId?: number } | null)?.roleId ?? 4;
|
||||
const where = config.accessFilter?.(viewerRoleId);
|
||||
if (where) {
|
||||
const match = await delegate.findFirst({ where: { id, ...where } });
|
||||
if (!match) return { success: false, error: 'Access denied' };
|
||||
}
|
||||
|
||||
await delegate.delete({ where: { id } });
|
||||
return { success: true };
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import type { PrismaScalarType } from './types';
|
||||
|
||||
export function extractFormData(
|
||||
formData: FormData,
|
||||
formFields: string[],
|
||||
fieldTypes: Record<string, PrismaScalarType>
|
||||
): Record<string, unknown> {
|
||||
const data: Record<string, unknown> = {};
|
||||
|
||||
for (const field of formFields) {
|
||||
const type = fieldTypes[field] ?? 'String';
|
||||
const raw = formData.get(field);
|
||||
|
||||
switch (type) {
|
||||
case 'Int': {
|
||||
const val = raw?.toString() ?? '';
|
||||
data[field] = val === '' ? null : parseInt(val, 10);
|
||||
break;
|
||||
}
|
||||
case 'Float': {
|
||||
const val = raw?.toString() ?? '';
|
||||
data[field] = val === '' ? null : parseFloat(val);
|
||||
break;
|
||||
}
|
||||
case 'Boolean': {
|
||||
data[field] = raw === 'on' || raw === 'true';
|
||||
break;
|
||||
}
|
||||
case 'DateTime': {
|
||||
const val = raw?.toString() ?? '';
|
||||
data[field] = val === '' ? null : new Date(val);
|
||||
break;
|
||||
}
|
||||
case 'Tiptap': {
|
||||
data[field] = raw?.toString() ?? '';
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
data[field] = raw?.toString() ?? '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export type { AdminModelConfig, RelationOptionConfig, AdminField, PrismaScalarType } from './types';
|
||||
export { ROLES, hasRole, canAccessAdmin } from './roles';
|
||||
export { fieldNameToLabel, resolveFieldValue } from './introspect';
|
||||
export { getDelegate, parseId } from './prisma-delegate';
|
||||
export { extractFormData } from './form-data';
|
||||
export { getModelConfig, getAllModelConfigs, getSerializableModelConfigs } from './registry';
|
||||
export type { SerializableModelConfig } from './registry';
|
||||
@@ -1,15 +0,0 @@
|
||||
export function fieldNameToLabel(fieldName: string): string {
|
||||
const withoutId = fieldName.replace(/Id$/, '');
|
||||
const spaced = withoutId.replace(/([a-z])([A-Z])/g, '$1 $2');
|
||||
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
||||
}
|
||||
|
||||
export function resolveFieldValue(record: Record<string, unknown>, path: string): unknown {
|
||||
const parts = path.split('.');
|
||||
let current: unknown = record;
|
||||
for (const part of parts) {
|
||||
if (current == null || typeof current !== 'object') return undefined;
|
||||
current = (current as Record<string, unknown>)[part];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { prisma } from '$lib/db';
|
||||
import type { AdminModelConfig } from './types';
|
||||
|
||||
function lcfirst(str: string): string {
|
||||
return str.charAt(0).toLowerCase() + str.slice(1);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function getDelegate(config: AdminModelConfig): any {
|
||||
const key = lcfirst(config.modelName);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const delegate = (prisma as any)[key];
|
||||
if (!delegate) {
|
||||
throw new Error(`Prisma delegate not found for model: ${config.modelName} (key: ${key})`);
|
||||
}
|
||||
return delegate;
|
||||
}
|
||||
|
||||
export function parseId(config: AdminModelConfig, raw: string): string | number {
|
||||
if (config.idType === 'Int') {
|
||||
const parsed = parseInt(raw, 10);
|
||||
if (isNaN(parsed)) throw new Error(`Invalid integer ID: ${raw}`);
|
||||
return parsed;
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
@@ -1,290 +0,0 @@
|
||||
import type { AdminModelConfig } from './types';
|
||||
import { ROLES } from './roles';
|
||||
|
||||
const models: AdminModelConfig[] = [
|
||||
{
|
||||
slug: 'users',
|
||||
modelName: 'User',
|
||||
displayName: 'Users',
|
||||
displayNameSingular: 'User',
|
||||
listOnly: true,
|
||||
idType: 'String',
|
||||
include: { role: true, profile: true, licenseKeys: true },
|
||||
listFields: ['name', 'email', 'emailVerified', 'role.name', 'createdAt'],
|
||||
searchFields: ['name', 'email'],
|
||||
formFields: ['roleId'],
|
||||
readonlyFields: ['id', 'name', 'email', 'emailVerified', 'createdAt', 'updatedAt'],
|
||||
fieldTypes: {
|
||||
name: 'String',
|
||||
email: 'String',
|
||||
emailVerified: 'Boolean',
|
||||
roleId: 'Int',
|
||||
createdAt: 'DateTime',
|
||||
updatedAt: 'DateTime'
|
||||
},
|
||||
relationOptions: {
|
||||
roleId: { model: 'UserRole', labelField: 'name', valueField: 'id' }
|
||||
},
|
||||
relatedRecords: [
|
||||
{
|
||||
relationKey: 'profile',
|
||||
label: 'Profile',
|
||||
type: 'has-one',
|
||||
displayFields: ['firstName', 'lastName', 'phoneNumber'],
|
||||
editSlug: 'profiles'
|
||||
},
|
||||
{
|
||||
relationKey: 'licenseKeys',
|
||||
label: 'License Keys',
|
||||
type: 'has-many',
|
||||
displayFields: ['key', 'active', 'level']
|
||||
}
|
||||
],
|
||||
accessFilter: (viewerRoleId: number) => {
|
||||
if (viewerRoleId <= ROLES.SUPER) return undefined;
|
||||
if (viewerRoleId <= ROLES.ADMIN) {
|
||||
return { roleId: { gte: ROLES.STAFF } };
|
||||
}
|
||||
return { roleId: { gt: ROLES.STAFF } };
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'profiles',
|
||||
modelName: 'Profile',
|
||||
displayName: 'Profiles',
|
||||
displayNameSingular: 'Profile',
|
||||
idType: 'String',
|
||||
include: { user: true, addresses: true },
|
||||
listFields: ['firstName', 'lastName', 'phoneNumber', 'user.name'],
|
||||
searchFields: ['firstName', 'lastName', 'phoneNumber'],
|
||||
formFields: ['userId', 'firstName', 'lastName', 'phoneNumber'],
|
||||
readonlyFields: ['id'],
|
||||
fieldTypes: {
|
||||
userId: 'String',
|
||||
firstName: 'String',
|
||||
lastName: 'String',
|
||||
phoneNumber: 'String'
|
||||
},
|
||||
relationOptions: {
|
||||
userId: { model: 'User', labelField: 'name', valueField: 'id' }
|
||||
},
|
||||
relatedRecords: [
|
||||
{
|
||||
relationKey: 'addresses',
|
||||
label: 'Addresses',
|
||||
type: 'has-many',
|
||||
displayFields: ['type', 'street', 'city', 'state', 'postalCode', 'country', 'isDefault']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
slug: 'user-roles',
|
||||
modelName: 'UserRole',
|
||||
displayName: 'User Roles',
|
||||
displayNameSingular: 'User Role',
|
||||
idType: 'Int',
|
||||
listFields: ['id', 'name'],
|
||||
searchFields: ['name'],
|
||||
formFields: ['name'],
|
||||
readonlyFields: ['id'],
|
||||
fieldTypes: {
|
||||
name: 'String'
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'proclamations',
|
||||
modelName: 'Proclamation',
|
||||
displayName: 'Proclamations',
|
||||
displayNameSingular: 'Proclamation',
|
||||
idType: 'String',
|
||||
include: {
|
||||
solfeggioFrequency: true,
|
||||
category: { include: { parent: true } },
|
||||
subscriptionLevel: true
|
||||
},
|
||||
listFields: ['name', 'category.name', 'solfeggioFrequency.name', 'subscriptionLevel.name'],
|
||||
searchFields: ['name', 'content'],
|
||||
formFields: [
|
||||
'name',
|
||||
'solfeggioFrequencyId',
|
||||
'parentCategoryId',
|
||||
'categoryId',
|
||||
'subscriptionLevelId',
|
||||
'content'
|
||||
],
|
||||
readonlyFields: ['id'],
|
||||
dataOnlyFields: ['parentCategoryId'],
|
||||
cascadeFields: { categoryId: { parentField: 'parentCategoryId' } },
|
||||
fieldTypes: {
|
||||
name: 'String',
|
||||
content: 'Tiptap',
|
||||
solfeggioFrequencyId: 'Int',
|
||||
parentCategoryId: 'Int',
|
||||
categoryId: 'Int',
|
||||
subscriptionLevelId: 'Int'
|
||||
},
|
||||
relationOptions: {
|
||||
solfeggioFrequencyId: {
|
||||
model: 'SolfeggioFrequency',
|
||||
labelField: 'name',
|
||||
valueField: 'id'
|
||||
},
|
||||
parentCategoryId: {
|
||||
model: 'ProclamationCategory',
|
||||
labelField: 'name',
|
||||
valueField: 'id'
|
||||
},
|
||||
categoryId: {
|
||||
model: 'ProclamationCategory',
|
||||
labelField: 'name',
|
||||
valueField: 'id'
|
||||
},
|
||||
subscriptionLevelId: {
|
||||
model: 'SubscriptionLevel',
|
||||
labelField: 'name',
|
||||
valueField: 'id'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'solfeggio-frequencies',
|
||||
modelName: 'SolfeggioFrequency',
|
||||
displayName: 'Solfeggio Frequencies',
|
||||
displayNameSingular: 'Solfeggio Frequency',
|
||||
idType: 'Int',
|
||||
include: { proclamations: true },
|
||||
listFields: ['name', 'value'],
|
||||
searchFields: ['name'],
|
||||
formFields: ['name', 'value'],
|
||||
readonlyFields: ['id'],
|
||||
fieldTypes: {
|
||||
name: 'String',
|
||||
value: 'Int'
|
||||
},
|
||||
relatedRecords: [
|
||||
{
|
||||
relationKey: 'proclamations',
|
||||
label: 'Proclamations',
|
||||
type: 'has-many',
|
||||
displayFields: ['name'],
|
||||
editSlug: 'proclamations'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
slug: 'subscription-levels',
|
||||
modelName: 'SubscriptionLevel',
|
||||
displayName: 'Subscription Levels',
|
||||
displayNameSingular: 'Subscription Level',
|
||||
idType: 'Int',
|
||||
include: { proclamations: true },
|
||||
listFields: ['id', 'name', 'description'],
|
||||
searchFields: ['name', 'description'],
|
||||
formFields: ['name', 'description'],
|
||||
readonlyFields: ['id'],
|
||||
fieldTypes: {
|
||||
name: 'String',
|
||||
description: 'String'
|
||||
},
|
||||
relatedRecords: [
|
||||
{
|
||||
relationKey: 'proclamations',
|
||||
label: 'Proclamations',
|
||||
type: 'has-many',
|
||||
displayFields: ['name'],
|
||||
editSlug: 'proclamations'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
slug: 'proclamation-categories',
|
||||
modelName: 'ProclamationCategory',
|
||||
displayName: 'Proclamation Categories',
|
||||
displayNameSingular: 'Proclamation Category',
|
||||
idType: 'Int',
|
||||
include: { proclamations: true, children: true },
|
||||
listFields: ['name'],
|
||||
searchFields: ['name'],
|
||||
formFields: ['name', 'parentId'],
|
||||
readonlyFields: ['id'],
|
||||
fieldTypes: {
|
||||
name: 'String',
|
||||
parentId: 'Int',
|
||||
sortOrder: 'Int'
|
||||
},
|
||||
relationOptions: {
|
||||
parentId: { model: 'ProclamationCategory', labelField: 'name', valueField: 'id' }
|
||||
},
|
||||
relatedRecords: [
|
||||
{
|
||||
relationKey: 'proclamations',
|
||||
label: 'Proclamations',
|
||||
type: 'has-many',
|
||||
displayFields: ['name'],
|
||||
editSlug: 'proclamations'
|
||||
},
|
||||
{
|
||||
relationKey: 'children',
|
||||
label: 'Sub-categories',
|
||||
type: 'has-many',
|
||||
displayFields: ['name'],
|
||||
editSlug: 'proclamation-categories'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
slug: 'license-keys',
|
||||
modelName: 'LicenseKey',
|
||||
displayName: 'License Keys',
|
||||
displayNameSingular: 'License Key',
|
||||
idType: 'String',
|
||||
include: { user: true },
|
||||
listFields: ['key', 'active', 'level', 'user.name'],
|
||||
searchFields: ['key', 'level'],
|
||||
formFields: ['active', 'level', 'userId'],
|
||||
readonlyFields: ['id'],
|
||||
fieldTypes: {
|
||||
key: 'String',
|
||||
active: 'Boolean',
|
||||
level: 'String',
|
||||
userId: 'String'
|
||||
},
|
||||
relationOptions: {
|
||||
userId: { model: 'User', labelField: 'name', valueField: 'id' }
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const modelsBySlug = new Map<string, AdminModelConfig>();
|
||||
for (const model of models) {
|
||||
modelsBySlug.set(model.slug, model);
|
||||
}
|
||||
|
||||
export function getModelConfig(slug: string): AdminModelConfig | undefined {
|
||||
return modelsBySlug.get(slug);
|
||||
}
|
||||
|
||||
export function getAllModelConfigs(): AdminModelConfig[] {
|
||||
return models;
|
||||
}
|
||||
|
||||
export type SerializableModelConfig = {
|
||||
slug: string;
|
||||
displayName: string;
|
||||
listFields: string[];
|
||||
accessFilterResult: Record<string, unknown> | undefined;
|
||||
};
|
||||
|
||||
export function getSerializableModelConfigs(
|
||||
viewerRoleId: number,
|
||||
excludeSlugs?: string[]
|
||||
): SerializableModelConfig[] {
|
||||
return models
|
||||
.filter((m) => !excludeSlugs?.includes(m.slug))
|
||||
.map((m) => ({
|
||||
slug: m.slug,
|
||||
displayName: m.displayName,
|
||||
listFields: m.listFields,
|
||||
accessFilterResult: m.accessFilter?.(viewerRoleId)
|
||||
}));
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
export const ROLES = {
|
||||
SUPER: 1,
|
||||
ADMIN: 2,
|
||||
STAFF: 3,
|
||||
USER: 4
|
||||
} as const;
|
||||
|
||||
export function hasRole(userRoleId: number | null | undefined, requiredRoleId: number): boolean {
|
||||
if (userRoleId == null || userRoleId < 1) return false;
|
||||
return userRoleId <= requiredRoleId;
|
||||
}
|
||||
|
||||
export function canAccessAdmin(userRoleId: number | null | undefined): boolean {
|
||||
return hasRole(userRoleId, ROLES.STAFF);
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const email = z.string().email('Invalid email address');
|
||||
const phone = z
|
||||
.string()
|
||||
.min(10, 'Phone number must be at least 10 digits')
|
||||
.regex(/^[\d\-\+\(\) ]+$/, 'Invalid phone number format');
|
||||
const name = z.string().min(1, 'Required').max(100, 'Too long');
|
||||
|
||||
export const UserRole = z.object({
|
||||
name: z.string().min(1, 'Required')
|
||||
});
|
||||
|
||||
export const User = z.object({
|
||||
name: z.string().min(1, 'Required'),
|
||||
email,
|
||||
roleId: z.coerce.number().int().min(1)
|
||||
});
|
||||
|
||||
export const Profile = z.object({
|
||||
userId: z.string().min(1, 'Required'),
|
||||
firstName: name,
|
||||
lastName: name,
|
||||
phoneNumber: phone
|
||||
});
|
||||
|
||||
export const Address = z.object({
|
||||
profileId: z.string().min(1),
|
||||
street: z.string().min(1, 'Street is required'),
|
||||
city: z.string().min(1, 'City is required'),
|
||||
state: z.string().min(1, 'State is required'),
|
||||
postalCode: z.string().min(1, 'Postal code is required'),
|
||||
country: z.string().min(1, 'Country is required'),
|
||||
type: z.enum(['Mailing', 'Billing', 'Shipping']).default('Mailing'),
|
||||
isDefaultMailing: z.coerce.boolean().default(false),
|
||||
isDefaultBilling: z.coerce.boolean().default(false),
|
||||
isDefaultShipping: z.coerce.boolean().default(false)
|
||||
});
|
||||
|
||||
export const SolfeggioFrequency = z.object({
|
||||
name: z.string().min(1, 'Required'),
|
||||
value: z.coerce.number().int('Must be a whole number')
|
||||
});
|
||||
|
||||
export const SubscriptionLevel = z.object({
|
||||
name: z.string().min(1, 'Required'),
|
||||
description: z.string().optional().default('')
|
||||
});
|
||||
|
||||
export const ProclamationCategory = z.object({
|
||||
name: z.string().min(1, 'Required'),
|
||||
parentId: z.coerce.number().int().optional()
|
||||
});
|
||||
|
||||
export const Proclamation = z.object({
|
||||
name: z.string().min(1, 'Required'),
|
||||
solfeggioFrequencyId: z.coerce.number().int(),
|
||||
parentCategoryId: z.coerce.number().int(),
|
||||
categoryId: z.coerce.number().int().optional(),
|
||||
subscriptionLevelId: z.coerce.number().int(),
|
||||
content: z.string().optional().default('')
|
||||
});
|
||||
|
||||
export const LicenseKey = z.object({
|
||||
active: z.coerce.boolean(),
|
||||
level: z.string().min(1, 'Required'),
|
||||
userId: z.string().min(1, 'Required')
|
||||
});
|
||||
|
||||
export const UserUpdate = z.object({
|
||||
id: z.string().min(1),
|
||||
name: z.string().min(1, 'Required').optional(),
|
||||
email: z.string().email().optional()
|
||||
});
|
||||
@@ -1,47 +0,0 @@
|
||||
export type PrismaScalarType = 'String' | 'Int' | 'Float' | 'Boolean' | 'DateTime' | 'Tiptap';
|
||||
|
||||
export type RelationOptionConfig = {
|
||||
model: string;
|
||||
labelField: string;
|
||||
valueField: string;
|
||||
};
|
||||
|
||||
export type AdminField = {
|
||||
name: string;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export type RelatedRecordConfig = {
|
||||
relationKey: string;
|
||||
label: string;
|
||||
type: 'has-one' | 'has-many';
|
||||
displayFields: string[];
|
||||
editSlug?: string;
|
||||
};
|
||||
|
||||
export type ActionResult = {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
fieldErrors?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type AdminModelConfig = {
|
||||
slug: string;
|
||||
modelName: string;
|
||||
displayName: string;
|
||||
displayNameSingular: string;
|
||||
idType: 'String' | 'Int';
|
||||
include?: Record<string, boolean | { include: Record<string, boolean> }>;
|
||||
listFields: string[];
|
||||
searchFields: string[];
|
||||
formFields: string[];
|
||||
readonlyFields?: string[];
|
||||
listOnly?: boolean;
|
||||
fieldTypes: Record<string, PrismaScalarType>;
|
||||
relationOptions?: Record<string, RelationOptionConfig>;
|
||||
relatedRecords?: RelatedRecordConfig[];
|
||||
accessFilter?: (viewerRoleId: number) => Record<string, unknown> | undefined;
|
||||
/** Fields that should be excluded from Prisma create/update (virtual/UI-only) */
|
||||
dataOnlyFields?: string[];
|
||||
cascadeFields?: Record<string, { parentField: string }>;
|
||||
};
|
||||
+14
-4
@@ -51,6 +51,7 @@ export function mockUserWithProfile(overrides?: Record<string, unknown>) {
|
||||
lastName: 'Doe',
|
||||
phoneNumber: '555-0100'
|
||||
},
|
||||
profiles: [{ id: 'profile-1', userId: 'user-1' }],
|
||||
...overrides
|
||||
});
|
||||
}
|
||||
@@ -76,6 +77,7 @@ export function mockUserWithProfileAndAddresses(overrides?: Record<string, unkno
|
||||
}
|
||||
]
|
||||
},
|
||||
profiles: [{ id: 'profile-1', userId: 'user-1' }],
|
||||
...overrides
|
||||
});
|
||||
}
|
||||
@@ -100,6 +102,7 @@ export function mockProclamation(overrides?: Record<string, unknown>) {
|
||||
content: '<p>Test content</p>',
|
||||
solfeggioFrequencyId: 1,
|
||||
categoryId: 1,
|
||||
groupId: 1,
|
||||
price: 0.99,
|
||||
solfeggioFrequency: {
|
||||
id: 1,
|
||||
@@ -108,9 +111,7 @@ export function mockProclamation(overrides?: Record<string, unknown>) {
|
||||
},
|
||||
category: {
|
||||
id: 1,
|
||||
name: 'Healing',
|
||||
parentId: null,
|
||||
parent: null
|
||||
name: 'Healing'
|
||||
},
|
||||
...overrides
|
||||
};
|
||||
@@ -121,7 +122,16 @@ export function mockCategory(overrides?: Record<string, unknown>) {
|
||||
return {
|
||||
id: 1,
|
||||
name: 'Healing',
|
||||
parentId: null,
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
/** Minimal proclamation group. */
|
||||
export function mockGroup(overrides?: Record<string, unknown>) {
|
||||
return {
|
||||
id: 1,
|
||||
name: 'General',
|
||||
sortOrder: 0,
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,20 +8,18 @@ export const load: PageServerLoad = async ({ locals }) => {
|
||||
throw error(403, 'not authorized');
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: locals.user.id },
|
||||
include: {
|
||||
profile: {
|
||||
include: { addresses: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
const user = await prisma.user.findUnique({ where: { id: locals.user.id } });
|
||||
|
||||
if (!user) {
|
||||
throw error(403, 'not authorized');
|
||||
}
|
||||
|
||||
return { user };
|
||||
const profile = await prisma.profile.findFirst({
|
||||
where: { userId: user.id },
|
||||
include: { addresses: true }
|
||||
});
|
||||
|
||||
return { user: { ...user, profile } };
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
@@ -40,7 +38,7 @@ export const actions: Actions = {
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.profile.update({
|
||||
await prisma.profile.updateMany({
|
||||
where: { userId: locals.user.id },
|
||||
data: { firstName, lastName, phoneNumber }
|
||||
});
|
||||
@@ -67,7 +65,7 @@ export const actions: Actions = {
|
||||
return fail(400, { error: 'All address fields are required.', action: 'addAddress' });
|
||||
}
|
||||
|
||||
const profile = await prisma.profile.findUnique({
|
||||
const profile = await prisma.profile.findFirst({
|
||||
where: { userId: locals.user.id },
|
||||
select: { id: true }
|
||||
});
|
||||
|
||||
@@ -23,67 +23,58 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full min-h-0 flex-col gap-8 p-6 dark:text-gray-100">
|
||||
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">Account</h1>
|
||||
<div class="mx-auto w-full max-w-3xl">
|
||||
<h1 class="text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl">Account</h1>
|
||||
<p class="mt-1 text-sm text-slate-500 sm:text-base">Manage your profile and addresses.</p>
|
||||
|
||||
<!-- Email (read-only) -->
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">Email</p>
|
||||
<p class="mt-1 text-gray-900 dark:text-gray-100">{data.user.email}</p>
|
||||
<div class="mt-6 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6">
|
||||
<p class="text-xs tracking-wide text-slate-500 uppercase">Email</p>
|
||||
<p class="mt-1 text-base break-words text-slate-900">{data.user.email}</p>
|
||||
</div>
|
||||
|
||||
<!-- Profile Form -->
|
||||
<section>
|
||||
<h2 class="mb-4 text-lg font-medium text-gray-900 dark:text-gray-100">Profile</h2>
|
||||
<section class="mt-6 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6">
|
||||
<h2 class="text-lg font-semibold text-slate-900">Profile</h2>
|
||||
|
||||
{#if form?.action === 'updateProfile' && form?.success}
|
||||
<div
|
||||
class="mb-4 rounded-lg border border-green-200 bg-green-50 p-3 text-green-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
|
||||
>
|
||||
<div class="mt-4 rounded-lg bg-green-50 px-4 py-3 text-sm text-green-800" role="status">
|
||||
Profile updated successfully.
|
||||
</div>
|
||||
{/if}
|
||||
{#if form?.action === 'updateProfile' && form?.error}
|
||||
<div
|
||||
class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
|
||||
>
|
||||
<div class="mt-4 rounded-lg bg-red-50 px-4 py-3 text-sm text-red-700" role="alert">
|
||||
{form.error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form method="POST" action="?/updateProfile" class="flex max-w-lg flex-col gap-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="firstName" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
First Name
|
||||
</label>
|
||||
<form method="POST" action="?/updateProfile" class="mt-5 flex flex-col gap-4">
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="firstName" class="text-sm font-medium text-slate-700"> First Name </label>
|
||||
<input
|
||||
type="text"
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
value={data.user.profile?.firstName ?? ''}
|
||||
required
|
||||
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="lastName" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Last Name
|
||||
</label>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="lastName" class="text-sm font-medium text-slate-700"> Last Name </label>
|
||||
<input
|
||||
type="text"
|
||||
id="lastName"
|
||||
name="lastName"
|
||||
value={data.user.profile?.lastName ?? ''}
|
||||
required
|
||||
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="phoneNumber" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Phone Number
|
||||
</label>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="phoneNumber" class="text-sm font-medium text-slate-700"> Phone Number </label>
|
||||
<input
|
||||
type="tel"
|
||||
id="phoneNumber"
|
||||
@@ -91,13 +82,13 @@
|
||||
bind:value={phoneNumber}
|
||||
oninput={handlePhoneInput}
|
||||
required
|
||||
class="mt-1 w-full max-w-xs rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-lg bg-blue-600 px-4 py-2 font-medium text-white transition-colors hover:bg-blue-700 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600"
|
||||
class="flex h-11 w-full items-center justify-center rounded-lg bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 sm:w-auto sm:px-8"
|
||||
>
|
||||
Save Profile
|
||||
</button>
|
||||
@@ -106,138 +97,112 @@
|
||||
</section>
|
||||
|
||||
<!-- Addresses -->
|
||||
<section>
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">Addresses</h2>
|
||||
<section class="mt-6 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<h2 class="text-lg font-semibold text-slate-900">Addresses</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg bg-green-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-green-700 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600"
|
||||
onclick={() => (showAddressForm = !showAddressForm)}
|
||||
class="rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50"
|
||||
>
|
||||
{showAddressForm ? 'Cancel' : 'Add Address'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if form?.action === 'addAddress' && form?.success}
|
||||
<div
|
||||
class="mb-4 rounded-lg border border-green-200 bg-green-50 p-3 text-green-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
|
||||
>
|
||||
<div class="mt-4 rounded-lg bg-green-50 px-4 py-3 text-sm text-green-800" role="status">
|
||||
Address added successfully.
|
||||
</div>
|
||||
{/if}
|
||||
{#if form?.action === 'addAddress' && form?.error}
|
||||
<div
|
||||
class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
|
||||
>
|
||||
<div class="mt-4 rounded-lg bg-red-50 px-4 py-3 text-sm text-red-700" role="alert">
|
||||
{form.error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showAddressForm}
|
||||
<form
|
||||
method="POST"
|
||||
action="?/addAddress"
|
||||
class="mb-6 max-w-lg rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div>
|
||||
<label for="street" class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>Street</label
|
||||
>
|
||||
<form method="POST" action="?/addAddress" class="mt-5 flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="street" class="text-sm font-medium text-slate-700">Street</label>
|
||||
<input
|
||||
type="text"
|
||||
id="street"
|
||||
name="street"
|
||||
required
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="city" class="text-sm font-medium text-slate-700">City</label>
|
||||
<input
|
||||
type="text"
|
||||
id="street"
|
||||
name="street"
|
||||
id="city"
|
||||
name="city"
|
||||
required
|
||||
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label for="city" class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>City</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="city"
|
||||
name="city"
|
||||
required
|
||||
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="state" class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>State</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="state"
|
||||
name="state"
|
||||
required
|
||||
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="state" class="text-sm font-medium text-slate-700">State</label>
|
||||
<input
|
||||
type="text"
|
||||
id="state"
|
||||
name="state"
|
||||
required
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label
|
||||
for="postalCode"
|
||||
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Postal Code
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="postalCode"
|
||||
name="postalCode"
|
||||
required
|
||||
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="country"
|
||||
class="block text-sm font-medium text-gray-700 dark:text-gray-300">Country</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="country"
|
||||
name="country"
|
||||
required
|
||||
value="US"
|
||||
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="postalCode" class="text-sm font-medium text-slate-700"> Postal Code </label>
|
||||
<input
|
||||
type="text"
|
||||
id="postalCode"
|
||||
name="postalCode"
|
||||
required
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-lg bg-blue-600 px-4 py-2 font-medium text-white transition-colors hover:bg-blue-700 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600"
|
||||
>
|
||||
Save Address
|
||||
</button>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="country" class="text-sm font-medium text-slate-700">Country</label>
|
||||
<input
|
||||
type="text"
|
||||
id="country"
|
||||
name="country"
|
||||
required
|
||||
value="US"
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
class="flex h-11 w-full items-center justify-center rounded-lg bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 sm:w-auto sm:px-8"
|
||||
>
|
||||
Save Address
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
{#if data.user.profile?.addresses && data.user.profile.addresses.length > 0}
|
||||
<div class="flex flex-col gap-3">
|
||||
{#each data.user.profile.addresses as address}
|
||||
<div
|
||||
class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-900"
|
||||
>
|
||||
<p class="text-sm text-gray-900 dark:text-gray-100">{address.street}</p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
<div class="mt-5 grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{#each data.user.profile.addresses as address (address.id)}
|
||||
<div class="rounded-lg border border-slate-200 bg-slate-50 p-4">
|
||||
<p class="text-sm font-medium text-slate-900">{address.street}</p>
|
||||
<p class="mt-1 text-sm text-slate-600">
|
||||
{address.city}, {address.state}
|
||||
{address.postalCode}
|
||||
</p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">{address.country}</p>
|
||||
<p class="text-sm text-slate-600">{address.country}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if !showAddressForm}
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">No addresses on file.</p>
|
||||
<p class="mt-5 text-sm text-slate-500">No addresses on file.</p>
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
vi.mock('$lib/db', () => ({
|
||||
prisma: {
|
||||
user: { findUnique: vi.fn() },
|
||||
profile: { update: vi.fn(), findUnique: vi.fn() },
|
||||
profile: { findFirst: vi.fn(), updateMany: vi.fn() },
|
||||
address: { create: vi.fn() }
|
||||
}
|
||||
}));
|
||||
@@ -19,8 +19,8 @@ import { prisma } from '$lib/db';
|
||||
import { load, actions } from './+page.server';
|
||||
|
||||
const userFindUnique = prisma.user.findUnique as ReturnType<typeof vi.fn>;
|
||||
const profileUpdate = prisma.profile.update as ReturnType<typeof vi.fn>;
|
||||
const profileFindUnique = prisma.profile.findUnique as ReturnType<typeof vi.fn>;
|
||||
const profileFindFirst = prisma.profile.findFirst as ReturnType<typeof vi.fn>;
|
||||
const profileUpdateMany = prisma.profile.updateMany as ReturnType<typeof vi.fn>;
|
||||
const addressCreate = prisma.address.create as ReturnType<typeof vi.fn>;
|
||||
|
||||
const invokeAction = (name: string, request: Request, locals: App.Locals) =>
|
||||
@@ -29,8 +29,8 @@ const invokeAction = (name: string, request: Request, locals: App.Locals) =>
|
||||
describe('(protected)/account page server', () => {
|
||||
beforeEach(() => {
|
||||
userFindUnique.mockReset();
|
||||
profileUpdate.mockReset();
|
||||
profileFindUnique.mockReset();
|
||||
profileFindFirst.mockReset();
|
||||
profileUpdateMany.mockReset();
|
||||
addressCreate.mockReset();
|
||||
});
|
||||
|
||||
@@ -51,24 +51,26 @@ describe('(protected)/account page server', () => {
|
||||
});
|
||||
|
||||
it('returns user with profile and addresses', async () => {
|
||||
const user = mockUserWithProfileAndAddresses();
|
||||
const { profile, ...user } = mockUserWithProfileAndAddresses() as any;
|
||||
userFindUnique.mockResolvedValue(user);
|
||||
profileFindFirst.mockResolvedValue(profile);
|
||||
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const result = (await load({ locals } as any)) as any;
|
||||
expect(result.user).toEqual(user);
|
||||
expect(result.user).toEqual({ ...user, profile });
|
||||
expect(result.user.profile.addresses).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('includes addresses in the query', async () => {
|
||||
userFindUnique.mockResolvedValue(mockUserWithProfileAndAddresses());
|
||||
it('loads the profile with addresses by user id', async () => {
|
||||
userFindUnique.mockResolvedValue(mockUser());
|
||||
profileFindFirst.mockResolvedValue({ id: 'profile-1' });
|
||||
|
||||
const locals = createLocals(mockUser() as any);
|
||||
await load({ locals } as any);
|
||||
|
||||
expect(userFindUnique).toHaveBeenCalledWith({
|
||||
where: { id: 'user-1' },
|
||||
include: { profile: { include: { addresses: true } } }
|
||||
expect(profileFindFirst).toHaveBeenCalledWith({
|
||||
where: { userId: 'user-1' },
|
||||
include: { addresses: true }
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -125,7 +127,7 @@ describe('(protected)/account page server', () => {
|
||||
});
|
||||
|
||||
it('updates profile with server-formatted phone number on success', async () => {
|
||||
profileUpdate.mockResolvedValue({});
|
||||
profileUpdateMany.mockResolvedValue({});
|
||||
|
||||
const request = createFormRequest({
|
||||
firstName: 'Jane',
|
||||
@@ -136,14 +138,14 @@ describe('(protected)/account page server', () => {
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const result = await invokeAction('updateProfile', request, locals);
|
||||
expect(result).toEqual({ success: true, action: 'updateProfile' });
|
||||
expect(profileUpdate).toHaveBeenCalledWith({
|
||||
expect(profileUpdateMany).toHaveBeenCalledWith({
|
||||
where: { userId: 'user-1' },
|
||||
data: { firstName: 'Jane', lastName: 'Smith', phoneNumber: '8175551234' }
|
||||
});
|
||||
});
|
||||
|
||||
it('returns fail(500) when prisma update throws', async () => {
|
||||
profileUpdate.mockRejectedValue(new Error('db error'));
|
||||
profileUpdateMany.mockRejectedValue(new Error('db error'));
|
||||
|
||||
const request = createFormRequest({
|
||||
firstName: 'Jane',
|
||||
@@ -190,7 +192,7 @@ describe('(protected)/account page server', () => {
|
||||
});
|
||||
|
||||
it('throws 400 when profile is not found', async () => {
|
||||
profileFindUnique.mockResolvedValue(null);
|
||||
profileFindFirst.mockResolvedValue(null);
|
||||
|
||||
const request = createFormRequest({
|
||||
street: '123 Main St',
|
||||
@@ -208,7 +210,7 @@ describe('(protected)/account page server', () => {
|
||||
});
|
||||
|
||||
it('creates address linked to profile on success', async () => {
|
||||
profileFindUnique.mockResolvedValue({ id: 'profile-1' });
|
||||
profileFindFirst.mockResolvedValue({ id: 'profile-1' });
|
||||
addressCreate.mockResolvedValue({});
|
||||
|
||||
const request = createFormRequest({
|
||||
@@ -235,7 +237,7 @@ describe('(protected)/account page server', () => {
|
||||
});
|
||||
|
||||
it('returns fail(500) when prisma create throws', async () => {
|
||||
profileFindUnique.mockResolvedValue({ id: 'profile-1' });
|
||||
profileFindFirst.mockResolvedValue({ id: 'profile-1' });
|
||||
addressCreate.mockRejectedValue(new Error('db error'));
|
||||
|
||||
const request = createFormRequest({
|
||||
|
||||
@@ -42,41 +42,74 @@
|
||||
</script>
|
||||
|
||||
<Window>
|
||||
<form method="POST" class="border p-6 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100">
|
||||
<p class="mx-auto my-4 text-center italic">Please finish setting up your profile</p>
|
||||
<form method="POST" class="flex flex-col items-center gap-5">
|
||||
<h1 class="text-center text-2xl font-bold text-slate-900">
|
||||
Please finish setting up your profile
|
||||
</h1>
|
||||
|
||||
{#if data.user?.image}
|
||||
<img src={data.user?.image} class="mx-auto w-[8rem] rounded-full" alt="Profile" />
|
||||
<img
|
||||
src={data.user?.image}
|
||||
alt="Profile"
|
||||
class="h-24 w-24 rounded-full object-cover shadow-sm sm:h-28 sm:w-28"
|
||||
/>
|
||||
{:else}
|
||||
<div class="mx-auto aspect-square w-[8rem] rounded-full bg-slate-900"></div>
|
||||
<div
|
||||
class="flex h-24 w-24 items-center justify-center rounded-full bg-indigo-100 text-3xl font-semibold text-indigo-700 sm:h-28 sm:w-28"
|
||||
>
|
||||
{data.user?.name?.charAt(0)?.toUpperCase() ?? '?'}
|
||||
</div>
|
||||
{/if}
|
||||
<p class="mx-auto my-4 text-center">{data.user?.email}</p>
|
||||
<div class="flex flex-col">
|
||||
<input type="text" name="userId" value={data.user?.id} class="hidden" />
|
||||
<input
|
||||
type="text"
|
||||
name="firstName"
|
||||
placeholder="First Name"
|
||||
class="my-2"
|
||||
bind:value={firstName}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name="lastName"
|
||||
placeholder="Last Name"
|
||||
class="my-2"
|
||||
bind:value={lastName}
|
||||
/>
|
||||
<input
|
||||
type="tel"
|
||||
name="phoneNumber"
|
||||
placeholder="Phone Number"
|
||||
class="my-2"
|
||||
bind:value={phoneNumber}
|
||||
oninput={handlePhoneInput}
|
||||
/>
|
||||
|
||||
<p class="text-base text-slate-900">{data.user?.email}</p>
|
||||
|
||||
<div class="flex w-full flex-col gap-4">
|
||||
<input type="hidden" name="userId" value={data.user?.id} />
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="firstName" class="text-sm font-medium text-slate-700"> First Name </label>
|
||||
<input
|
||||
id="firstName"
|
||||
type="text"
|
||||
name="firstName"
|
||||
placeholder="First Name"
|
||||
bind:value={firstName}
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="lastName" class="text-sm font-medium text-slate-700"> Last Name </label>
|
||||
<input
|
||||
id="lastName"
|
||||
type="text"
|
||||
name="lastName"
|
||||
placeholder="Last Name"
|
||||
bind:value={lastName}
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="phoneNumber" class="text-sm font-medium text-slate-700"> Phone Number </label>
|
||||
<input
|
||||
id="phoneNumber"
|
||||
type="tel"
|
||||
name="phoneNumber"
|
||||
placeholder="(555) 555-5555"
|
||||
bind:value={phoneNumber}
|
||||
oninput={handlePhoneInput}
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="bg-blue-300 p-2 px-4 dark:bg-gray-700 dark:text-gray-100"
|
||||
>Submit</button
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="mt-1 flex h-11 w-full items-center justify-center rounded-lg bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
</Window>
|
||||
|
||||
@@ -7,19 +7,16 @@ export const load: PageServerLoad = async ({ locals }) => {
|
||||
throw error(403, 'not authorized');
|
||||
}
|
||||
|
||||
const profile = await prisma.profile.findUnique({
|
||||
const profile = await prisma.profile.findFirst({
|
||||
where: { userId: locals.user.id },
|
||||
select: { firstName: true }
|
||||
});
|
||||
|
||||
const [keyStats, proclamationCount] = await Promise.all([
|
||||
prisma.licenseKey.groupBy({
|
||||
by: ['active'],
|
||||
where: { userId: locals.user.id },
|
||||
_count: true
|
||||
}),
|
||||
prisma.proclamation.count()
|
||||
]);
|
||||
const keyStats = await prisma.licenseKey.groupBy({
|
||||
by: ['active'],
|
||||
where: { userId: locals.user.id },
|
||||
_count: true
|
||||
});
|
||||
|
||||
const activeKeys = keyStats.find((s) => s.active)?._count ?? 0;
|
||||
const totalKeys = keyStats.reduce((sum, s) => sum + s._count, 0);
|
||||
@@ -27,7 +24,6 @@ export const load: PageServerLoad = async ({ locals }) => {
|
||||
return {
|
||||
firstName: profile?.firstName ?? 'User',
|
||||
activeKeys,
|
||||
totalKeys,
|
||||
proclamationCount
|
||||
totalKeys
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,42 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex h-full min-h-0 flex-col gap-6 p-6 dark:text-gray-100">
|
||||
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl">
|
||||
Welcome, {data.firstName}!
|
||||
</h1>
|
||||
<p class="mt-1 text-sm text-slate-500 sm:text-base">Here's an overview of your account.</p>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<div class="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<a
|
||||
href="/license-keys"
|
||||
class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-900"
|
||||
href={resolve('/license-keys')}
|
||||
class="group flex flex-col gap-1 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition hover:border-indigo-200 hover:shadow"
|
||||
>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Active License Keys</p>
|
||||
<p class="mt-2 text-3xl font-bold text-gray-900 dark:text-gray-100">{data.activeKeys}</p>
|
||||
<p class="mt-1 text-sm text-gray-400 dark:text-gray-500">{data.totalKeys} total</p>
|
||||
<span class="text-sm font-medium text-slate-600">Active License Keys</span>
|
||||
<span class="text-3xl font-bold text-indigo-600">{data.activeKeys}</span>
|
||||
<span class="text-sm text-slate-500">{data.totalKeys} total</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/admin/proclamations"
|
||||
class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-900"
|
||||
href={resolve('/account')}
|
||||
class="group flex flex-col gap-1 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition hover:border-indigo-200 hover:shadow"
|
||||
>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Proclamations</p>
|
||||
<p class="mt-2 text-3xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{data.proclamationCount}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-gray-400 dark:text-gray-500">Browse all</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/account"
|
||||
class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-900"
|
||||
>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Account</p>
|
||||
<p class="mt-2 text-lg font-semibold text-gray-900 dark:text-gray-100">Manage Profile</p>
|
||||
<p class="mt-1 text-sm text-gray-400 dark:text-gray-500">Edit your details</p>
|
||||
<span class="text-sm font-medium text-slate-600">Account</span>
|
||||
<span class="text-2xl font-semibold text-slate-900">Manage Profile</span>
|
||||
<span class="text-sm text-slate-500">Edit your details</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,24 +4,21 @@ import { createLocals, mockUser } from '$lib/test-utils';
|
||||
|
||||
vi.mock('$lib/db', () => ({
|
||||
prisma: {
|
||||
profile: { findUnique: vi.fn() },
|
||||
licenseKey: { groupBy: vi.fn() },
|
||||
proclamation: { count: vi.fn() }
|
||||
profile: { findFirst: vi.fn() },
|
||||
licenseKey: { groupBy: vi.fn() }
|
||||
}
|
||||
}));
|
||||
|
||||
import { prisma } from '$lib/db';
|
||||
import { load } from './+page.server';
|
||||
|
||||
const profileFindUnique = prisma.profile.findUnique as ReturnType<typeof vi.fn>;
|
||||
const profileFindFirst = prisma.profile.findFirst as ReturnType<typeof vi.fn>;
|
||||
const licenseKeyGroupBy = prisma.licenseKey.groupBy as ReturnType<typeof vi.fn>;
|
||||
const proclamationCount = prisma.proclamation.count as ReturnType<typeof vi.fn>;
|
||||
|
||||
describe('(protected)/dashboard page server load', () => {
|
||||
beforeEach(() => {
|
||||
profileFindUnique.mockReset();
|
||||
profileFindFirst.mockReset();
|
||||
licenseKeyGroupBy.mockReset();
|
||||
proclamationCount.mockReset();
|
||||
});
|
||||
|
||||
it('throws 403 when locals.user is undefined', async () => {
|
||||
@@ -32,9 +29,8 @@ describe('(protected)/dashboard page server load', () => {
|
||||
});
|
||||
|
||||
it('returns firstName from profile', async () => {
|
||||
profileFindUnique.mockResolvedValue({ firstName: 'Jane' });
|
||||
profileFindFirst.mockResolvedValue({ firstName: 'Jane' });
|
||||
licenseKeyGroupBy.mockResolvedValue([]);
|
||||
proclamationCount.mockResolvedValue(0);
|
||||
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const result = (await load({ locals } as any)) as any;
|
||||
@@ -42,9 +38,8 @@ describe('(protected)/dashboard page server load', () => {
|
||||
});
|
||||
|
||||
it('defaults firstName to "User" when profile is null', async () => {
|
||||
profileFindUnique.mockResolvedValue(null);
|
||||
profileFindFirst.mockResolvedValue(null);
|
||||
licenseKeyGroupBy.mockResolvedValue([]);
|
||||
proclamationCount.mockResolvedValue(0);
|
||||
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const result = (await load({ locals } as any)) as any;
|
||||
@@ -52,12 +47,11 @@ describe('(protected)/dashboard page server load', () => {
|
||||
});
|
||||
|
||||
it('computes activeKeys and totalKeys from groupBy results', async () => {
|
||||
profileFindUnique.mockResolvedValue({ firstName: 'Jane' });
|
||||
profileFindFirst.mockResolvedValue({ firstName: 'Jane' });
|
||||
licenseKeyGroupBy.mockResolvedValue([
|
||||
{ active: true, _count: 3 },
|
||||
{ active: false, _count: 2 }
|
||||
]);
|
||||
proclamationCount.mockResolvedValue(10);
|
||||
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const result = (await load({ locals } as any)) as any;
|
||||
@@ -66,23 +60,12 @@ describe('(protected)/dashboard page server load', () => {
|
||||
});
|
||||
|
||||
it('returns 0 for activeKeys when none are active', async () => {
|
||||
profileFindUnique.mockResolvedValue({ firstName: 'Jane' });
|
||||
profileFindFirst.mockResolvedValue({ firstName: 'Jane' });
|
||||
licenseKeyGroupBy.mockResolvedValue([{ active: false, _count: 4 }]);
|
||||
proclamationCount.mockResolvedValue(0);
|
||||
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const result = (await load({ locals } as any)) as any;
|
||||
expect(result.activeKeys).toBe(0);
|
||||
expect(result.totalKeys).toBe(4);
|
||||
});
|
||||
|
||||
it('returns proclamationCount', async () => {
|
||||
profileFindUnique.mockResolvedValue({ firstName: 'Jane' });
|
||||
licenseKeyGroupBy.mockResolvedValue([]);
|
||||
proclamationCount.mockResolvedValue(42);
|
||||
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const result = (await load({ locals } as any)) as any;
|
||||
expect(result.proclamationCount).toBe(42);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,47 +4,35 @@
|
||||
let { data }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex h-full min-h-0 flex-col gap-6 p-6 dark:text-gray-100">
|
||||
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">License Keys</h1>
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl">License Keys</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Your active and inactive license keys.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if data.licenseKeys.length > 0}
|
||||
<div class="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead class="bg-gray-50 dark:bg-gray-800">
|
||||
<div class="mt-6 overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
|
||||
<!-- Desktop / tablet: table -->
|
||||
<table class="hidden w-full text-left text-sm sm:table">
|
||||
<thead
|
||||
class="border-b border-slate-200 bg-slate-50 text-xs tracking-wide text-slate-500 uppercase"
|
||||
>
|
||||
<tr>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase dark:text-gray-400"
|
||||
>
|
||||
Key
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase dark:text-gray-400"
|
||||
>
|
||||
Level
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase dark:text-gray-400"
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th class="px-6 py-3 font-semibold">Key</th>
|
||||
<th class="px-6 py-3 font-semibold">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-900">
|
||||
{#each data.licenseKeys as key}
|
||||
<tr>
|
||||
<td
|
||||
class="px-6 py-4 font-mono text-sm whitespace-nowrap text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
{key.key}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-700 dark:text-gray-300">
|
||||
{key.level}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm whitespace-nowrap">
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{#each data.licenseKeys as key (key.key)}
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="px-6 py-4 font-mono text-slate-800">{key.key}</td>
|
||||
<td class="px-6 py-4">
|
||||
<span
|
||||
class="inline-flex rounded-full px-2 text-xs leading-5 font-semibold {key.active
|
||||
? 'bg-green-100 text-green-800 dark:bg-gray-700 dark:text-gray-200'
|
||||
: 'bg-red-100 text-red-800 dark:bg-gray-700 dark:text-gray-200'}"
|
||||
class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium {key.active
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-slate-100 text-slate-600'}"
|
||||
>
|
||||
{key.active ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
@@ -53,10 +41,26 @@
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Mobile: card list -->
|
||||
<ul class="divide-y divide-slate-100 sm:hidden">
|
||||
{#each data.licenseKeys as key (key.key)}
|
||||
<li class="flex items-center justify-between gap-4 px-4 py-4">
|
||||
<span class="min-w-0 font-mono text-sm break-all text-slate-800">{key.key}</span>
|
||||
<span
|
||||
class="shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium {key.active
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-slate-100 text-slate-600'}"
|
||||
>
|
||||
{key.active ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-1 items-center justify-center">
|
||||
<p class="text-lg text-gray-500 dark:text-gray-400">You don't have any license keys yet.</p>
|
||||
<div class="mt-6 rounded-2xl border border-dashed border-slate-300 bg-white p-8 text-center">
|
||||
<p class="text-sm text-slate-600">You don't have any license keys yet.</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -9,12 +9,12 @@ export const load: LayoutServerLoad = async ({ locals, url }) => {
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: locals.user.id },
|
||||
select: { id: true, roleId: true, profile: { select: { id: true } } }
|
||||
select: { id: true, roleId: true, profiles: { select: { id: true } } }
|
||||
});
|
||||
if (!user) {
|
||||
throw error(403, 'not authorized');
|
||||
}
|
||||
if (!user.profile && !url.pathname.startsWith('/account/setup')) {
|
||||
if (user.profiles.length === 0 && !url.pathname.startsWith('/account/setup')) {
|
||||
// Skip redirect when already on /account/setup to avoid redirect loop
|
||||
throw redirect(303, '/account/setup');
|
||||
}
|
||||
|
||||
@@ -1,74 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { canAccessAdmin } from '$lib/svelte-admin/roles';
|
||||
import { getAllModelConfigs } from '$lib/svelte-admin/registry';
|
||||
import type { LayoutData } from './$types';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
let { children, data } = $props<{ children: any; data: LayoutData }>();
|
||||
|
||||
const isAdmin = $derived(canAccessAdmin(data.userRoleId));
|
||||
const adminModels = getAllModelConfigs();
|
||||
|
||||
const navLinks = [
|
||||
{ href: '/dashboard', label: 'Dashboard' },
|
||||
{ href: '/account', label: 'Account' },
|
||||
{ href: '/proclamations', label: 'Proclamations' },
|
||||
{ href: '/license-keys', label: 'License Keys' }
|
||||
];
|
||||
|
||||
function isActive(href: string): boolean {
|
||||
return page.url.pathname === href || page.url.pathname.startsWith(href + '/');
|
||||
}
|
||||
let { children }: { children: Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-0 flex-1">
|
||||
<!-- Sidebar -->
|
||||
<nav class="mt-12 mr-12 flex flex-col gap-1">
|
||||
{#each navLinks as link}
|
||||
<a
|
||||
href={link.href}
|
||||
class="rounded px-3 py-1.5 text-sm font-medium transition-colors {isActive(link.href)
|
||||
? 'bg-blue-50 text-blue-700 dark:bg-gray-800 dark:text-gray-100'
|
||||
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100'}"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
{/each}
|
||||
{#if isAdmin}
|
||||
<div class="mt-4 flex flex-col gap-1">
|
||||
<a
|
||||
href="/admin"
|
||||
class="rounded px-3 py-1.5 text-sm font-medium transition-colors {isActive('/admin')
|
||||
? 'bg-blue-50 text-blue-700 dark:bg-gray-800 dark:text-gray-100'
|
||||
: 'text-gray-700 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100'}"
|
||||
>
|
||||
Admin Panel
|
||||
</a>
|
||||
<div class="ml-3 flex flex-col gap-0.5">
|
||||
{#each adminModels as model}
|
||||
<a
|
||||
href="/admin/{model.slug}"
|
||||
class="rounded px-3 py-1 text-sm transition-colors {isActive('/admin/' + model.slug)
|
||||
? 'bg-blue-50 text-blue-700 dark:bg-gray-800 dark:text-gray-100'
|
||||
: 'text-blue-600 hover:bg-gray-50 hover:text-blue-800 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100'}"
|
||||
>
|
||||
{model.displayName}
|
||||
</a>
|
||||
{/each}
|
||||
<a
|
||||
href="/admin/api-keys"
|
||||
class="rounded px-3 py-1 text-sm transition-colors {isActive('/admin/api-keys')
|
||||
? 'bg-blue-50 text-blue-700 dark:bg-gray-800 dark:text-gray-100'
|
||||
: 'text-blue-600 hover:bg-gray-50 hover:text-blue-800 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100'}"
|
||||
>
|
||||
API Keys
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</nav>
|
||||
<!-- Page content -->
|
||||
<div class="min-h-0 flex-1 overflow-auto">
|
||||
<div class="min-h-screen bg-slate-50 text-slate-900">
|
||||
<main class="mx-auto w-full max-w-6xl px-4 py-6 sm:px-6 sm:py-8">
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -4,22 +4,17 @@
|
||||
</script>
|
||||
|
||||
<Window>
|
||||
<div class="flex flex-col items-center gap-4 p-6 text-center">
|
||||
<h1 class="text-xl font-bold text-gray-900 dark:text-gray-100">
|
||||
<div>
|
||||
<h1>
|
||||
Admin Error ({$page.status})
|
||||
</h1>
|
||||
<p class="max-w-md text-sm text-gray-600 dark:text-gray-400">
|
||||
<p>
|
||||
{#if $page.error?.message}
|
||||
{$page.error.message}
|
||||
{:else}
|
||||
An unexpected error occurred while loading this admin page.
|
||||
{/if}
|
||||
</p>
|
||||
<a
|
||||
href="/admin"
|
||||
class="mt-2 border border-gray-200 px-4 py-2 text-sm font-medium text-gray-900 hover:bg-gray-50 dark:border-gray-700 dark:text-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
Back to Admin
|
||||
</a>
|
||||
<a href="/admin"> Back to Admin </a>
|
||||
</div>
|
||||
</Window>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { prisma } from '$lib/db';
|
||||
import { canAccessAdmin } from '$lib/svelte-admin/roles';
|
||||
|
||||
/**
|
||||
* This Layout Server-Side page checks if the user is logged in
|
||||
@@ -17,7 +16,7 @@ export const load: LayoutServerLoad = async ({ locals }) => {
|
||||
select: { id: true, name: true, roleId: true, role: { select: { name: true } } }
|
||||
});
|
||||
|
||||
if (!user || !canAccessAdmin(user.roleId)) {
|
||||
if (!user || !user.roleId || user.roleId > 3) {
|
||||
console.log(
|
||||
`Attempted to access /admin via ${user?.id} : ${user?.name} with a role of ${user?.role}`
|
||||
);
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getSerializableModelConfigs } from '$lib/svelte-admin/registry';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
const viewerRoleId = (locals.user as { roleId?: number } | null)?.roleId ?? 4;
|
||||
const models = getSerializableModelConfigs(viewerRoleId, ['license-keys']);
|
||||
return { models };
|
||||
export const load = () => {
|
||||
redirect(307, '/admin/profiles');
|
||||
};
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
let models = $derived(data.models);
|
||||
</script>
|
||||
|
||||
<h1 class="p-6 text-xl font-bold text-gray-900 dark:text-gray-100">Admin Dashboard</h1>
|
||||
<div class="grid grid-cols-1 gap-4 px-6 pb-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each models as model (model.slug)}
|
||||
<a
|
||||
href={'/admin/' + model.slug}
|
||||
class="block border border-gray-200 p-4 hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800"
|
||||
>
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">{model.displayName}</h2>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{model.listFields.length} fields
|
||||
</p>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -2,11 +2,12 @@ import type { PageServerLoad } from './$types';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { prisma } from '$lib/db';
|
||||
import { rotateApiKey } from '$lib/server/api-key';
|
||||
import { ROLES } from '$lib/svelte-admin/roles';
|
||||
|
||||
const SUPER = 1;
|
||||
|
||||
export const load: PageServerLoad = async ({ locals, parent }) => {
|
||||
const { adminUser } = await parent();
|
||||
if (adminUser.roleId > ROLES.SUPER) {
|
||||
if (adminUser.roleId > SUPER) {
|
||||
throw error(403, 'Only Super admins can manage API keys');
|
||||
}
|
||||
|
||||
@@ -49,7 +50,7 @@ async function viewerRoleId(locals: App.Locals): Promise<number> {
|
||||
|
||||
export const actions = {
|
||||
rotate: async ({ locals }) => {
|
||||
if ((await viewerRoleId(locals)) > ROLES.SUPER) {
|
||||
if ((await viewerRoleId(locals)) > SUPER) {
|
||||
return { success: false, error: 'Not authorized' };
|
||||
}
|
||||
|
||||
@@ -62,7 +63,7 @@ export const actions = {
|
||||
};
|
||||
},
|
||||
rotateEmergency: async ({ locals }) => {
|
||||
if ((await viewerRoleId(locals)) > ROLES.SUPER) {
|
||||
if ((await viewerRoleId(locals)) > SUPER) {
|
||||
return { success: false, error: 'Not authorized' };
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
let mode = $state<'normal' | 'emergency' | null>(null);
|
||||
let expiryDate = $state<string | null>(null);
|
||||
let toastMessage = $state('');
|
||||
let toastType = $state<'success' | 'error'>('success');
|
||||
|
||||
function handleRotate(emergency: boolean) {
|
||||
return ({ cancel }: any) => {
|
||||
@@ -29,7 +28,6 @@
|
||||
}
|
||||
} else {
|
||||
toastMessage = 'Rotation failed';
|
||||
toastType = 'error';
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -38,7 +36,6 @@
|
||||
function copyKey() {
|
||||
navigator.clipboard.writeText(newKey);
|
||||
toastMessage = 'Copied to clipboard';
|
||||
toastType = 'success';
|
||||
}
|
||||
|
||||
function dismissKey() {
|
||||
@@ -48,44 +45,45 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<h1 class="mb-4 text-xl font-bold text-gray-900 dark:text-gray-100">API Key Management</h1>
|
||||
<div class="mx-auto w-full">
|
||||
<div class="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h2 class="text-xl font-semibold text-slate-900">API Key Management</h2>
|
||||
{#if data.activeKey}
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-full bg-green-100 px-3 py-1 text-xs font-medium text-green-700"
|
||||
>
|
||||
Active
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if newKey}
|
||||
<div
|
||||
class="mb-6 border border-amber-300 bg-amber-50 p-4 dark:border-amber-700 dark:bg-amber-900/30"
|
||||
>
|
||||
<p class="mb-2 text-sm font-semibold text-amber-800 dark:text-amber-300">
|
||||
<div class="mb-4 rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
|
||||
<p class="text-sm font-medium text-slate-900">
|
||||
{mode === 'emergency' ? 'Emergency Rotation Complete' : 'New API Key Generated'}
|
||||
</p>
|
||||
<p class="mb-1 text-xs text-amber-700 dark:text-amber-400">
|
||||
This key will not be shown again. Copy it now.
|
||||
</p>
|
||||
<div class="mb-3">
|
||||
<code
|
||||
class="rounded border border-amber-300 bg-white px-3 py-2 font-mono text-sm break-all dark:border-amber-600 dark:bg-gray-900 dark:text-gray-100"
|
||||
>
|
||||
{newKey}
|
||||
</code>
|
||||
<p class="mt-1 text-sm text-slate-600">This key will not be shown again. Copy it now.</p>
|
||||
<div class="mt-3 rounded-md bg-slate-100 px-3 py-2">
|
||||
<code class="text-sm break-all text-slate-800">{newKey}</code>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="mt-3 flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onclick={copyKey}
|
||||
class="border border-amber-300 bg-white px-3 py-1.5 text-sm font-medium hover:bg-amber-100 dark:border-amber-600 dark:bg-gray-800 dark:text-amber-300 dark:hover:bg-amber-900/50"
|
||||
class="inline-flex h-10 items-center justify-center gap-1.5 rounded-md bg-indigo-600 px-4 text-sm font-medium text-white shadow-sm transition hover:bg-indigo-700"
|
||||
>
|
||||
Copy to Clipboard
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={dismissKey}
|
||||
class="border border-gray-300 px-3 py-1.5 text-sm dark:border-gray-600 dark:text-gray-300"
|
||||
class="inline-flex h-10 items-center justify-center rounded-md border border-slate-300 bg-white px-4 text-sm font-medium text-slate-700 shadow-sm transition hover:bg-slate-50"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
{#if mode === 'normal' && expiryDate}
|
||||
<p class="mt-3 text-xs text-amber-700 dark:text-amber-400">
|
||||
<p class="mt-3 text-xs text-slate-500">
|
||||
The previous key will remain valid until {new Date(expiryDate).toLocaleString()}
|
||||
to give in-field devices time to update.
|
||||
</p>
|
||||
@@ -95,22 +93,18 @@
|
||||
|
||||
{#if toastMessage}
|
||||
<div
|
||||
class="mb-4 border px-4 py-2 text-sm {toastType === 'success'
|
||||
? 'border-green-300 bg-green-50 text-green-700'
|
||||
: 'border-red-300 bg-red-50 text-red-700'}"
|
||||
class="mb-4 rounded-md border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700"
|
||||
>
|
||||
{toastMessage}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mb-6 rounded border border-gray-200 p-4 dark:border-gray-700">
|
||||
<h2 class="mb-3 text-sm font-semibold text-gray-700 dark:text-gray-300">Current Key Status</h2>
|
||||
<div class="mb-4 rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
|
||||
<h3 class="text-base font-semibold text-slate-900">Current Key Status</h3>
|
||||
{#if data.activeKey}
|
||||
<div class="space-y-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
<div class="mt-3 space-y-1 text-sm text-slate-700">
|
||||
<div>
|
||||
Prefix: <code class="font-mono font-medium text-gray-900 dark:text-gray-100"
|
||||
>{data.activeKey.prefix}</code
|
||||
>
|
||||
Prefix: <code class="rounded bg-slate-100 px-1.5 py-0.5">{data.activeKey.prefix}</code>
|
||||
</div>
|
||||
<div>Created: {new Date(data.activeKey.createdAt).toLocaleString()}</div>
|
||||
{#if data.activeKey.expiresAt}
|
||||
@@ -118,20 +112,20 @@
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">No active API key. Generate one below.</p>
|
||||
<p class="mt-2 text-sm text-slate-600">No active API key. Generate one below.</p>
|
||||
{/if}
|
||||
{#if data.totalRotated > 0}
|
||||
<p class="mt-2 text-xs text-gray-400">
|
||||
<p class="mt-2 text-sm text-slate-600">
|
||||
{data.totalRotated} previous key(s) have been rotated.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<div class="flex flex-col gap-3 sm:flex-row">
|
||||
<form method="POST" action="?/rotate" use:enhance={handleRotate(false)}>
|
||||
<button
|
||||
type="submit"
|
||||
class="border border-gray-300 bg-gray-100 px-4 py-2 text-sm font-medium hover:bg-gray-200 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
class="inline-flex h-10 items-center justify-center gap-1.5 rounded-md bg-indigo-600 px-4 text-sm font-medium text-white shadow-sm transition hover:bg-indigo-700"
|
||||
>
|
||||
Generate New API Key
|
||||
</button>
|
||||
@@ -139,7 +133,7 @@
|
||||
<form method="POST" action="?/rotateEmergency" use:enhance={handleRotate(true)}>
|
||||
<button
|
||||
type="submit"
|
||||
class="border border-red-300 bg-red-50 px-4 py-2 text-sm font-medium text-red-700 hover:bg-red-100 dark:border-red-700 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50"
|
||||
class="inline-flex h-10 items-center justify-center gap-1.5 rounded-md bg-red-600 px-4 text-sm font-medium text-white shadow-sm transition hover:bg-red-700"
|
||||
>
|
||||
Emergency Rotation
|
||||
</button>
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getModelConfig } from '$lib/svelte-admin/registry';
|
||||
import { loadRecords } from '$lib/svelte-admin/crud';
|
||||
import { prisma } from '$lib/db';
|
||||
import { sendMail } from '$lib/server/resend';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
const config = getModelConfig('license-keys')!;
|
||||
function generateLicenseKey(): string {
|
||||
const hex = crypto.randomUUID().replace(/-/g, '').slice(0, 16).toUpperCase();
|
||||
return `${hex.slice(0, 4)}-${hex.slice(4, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}`;
|
||||
}
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
const records = await loadRecords(locals, config);
|
||||
|
||||
const subscriptionLevels = await prisma.subscriptionLevel.findMany({
|
||||
select: { id: true, name: true },
|
||||
orderBy: { name: 'asc' }
|
||||
export const load: PageServerLoad = async () => {
|
||||
const records = await prisma.licenseKey.findMany({
|
||||
include: { user: true },
|
||||
orderBy: { key: 'asc' }
|
||||
});
|
||||
|
||||
return {
|
||||
...records,
|
||||
subscriptionLevels: subscriptionLevels.map((s: { id: number; name: string }) => ({
|
||||
label: s.name,
|
||||
value: s.name
|
||||
}))
|
||||
};
|
||||
return { records: records as unknown as Record<string, unknown>[] };
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
@@ -29,41 +21,35 @@ export const actions = {
|
||||
if (!locals.user) throw error(401, 'Unauthorized');
|
||||
|
||||
const formData = await request.formData();
|
||||
const email = formData.get('email')?.toString();
|
||||
const level = formData.get('level')?.toString();
|
||||
const email = formData.get('email')?.toString()?.trim().toLowerCase();
|
||||
|
||||
if (!email) {
|
||||
return { success: false, error: 'Email is required' };
|
||||
}
|
||||
if (!level) {
|
||||
return { success: false, error: 'Subscription level is required' };
|
||||
return { success: false, action: 'createKey', error: 'Email is required' };
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { email } });
|
||||
if (!user) {
|
||||
return { success: false, error: 'No user found with that email' };
|
||||
}
|
||||
|
||||
const hex = crypto.randomUUID().replace(/-/g, '').slice(0, 16).toUpperCase();
|
||||
const key = `${hex.slice(0, 4)}-${hex.slice(4, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}`;
|
||||
const key = generateLicenseKey();
|
||||
|
||||
await prisma.licenseKey.create({
|
||||
data: {
|
||||
key,
|
||||
active: true,
|
||||
level,
|
||||
userId: user.id
|
||||
userId: user?.id,
|
||||
email: user ? undefined : email
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const profile = await prisma.profile.findUnique({ where: { userId: user.id } });
|
||||
const profile = await prisma.profile.findFirst({
|
||||
where: user ? { userId: user.id } : { email }
|
||||
});
|
||||
await sendMail({
|
||||
to: email,
|
||||
subject: 'Your Rhemawave License Key',
|
||||
subject: 'Your Radiant Wave License Key',
|
||||
templateId: 'license-key-delivery',
|
||||
data: {
|
||||
firstName: profile?.firstName ?? user.name ?? 'Valued Customer',
|
||||
firstName: profile?.firstName ?? user?.name ?? 'Valued Customer',
|
||||
license_key: key
|
||||
}
|
||||
});
|
||||
@@ -71,32 +57,29 @@ export const actions = {
|
||||
console.error('Failed to send license key email:', e);
|
||||
}
|
||||
|
||||
return { success: true, error: undefined };
|
||||
return { success: true, action: 'createKey' };
|
||||
},
|
||||
replace: async ({ request, locals }) => {
|
||||
if (!locals.user) throw error(401, 'Unauthorized');
|
||||
|
||||
const formData = await request.formData();
|
||||
const keyId = formData.get('id')?.toString();
|
||||
if (!keyId) return { success: false, error: 'Missing key id' };
|
||||
if (!keyId) return { success: false, action: 'replace', error: 'Missing key id' };
|
||||
|
||||
const existing = await prisma.licenseKey.findUnique({ where: { id: keyId } });
|
||||
if (!existing) return { success: false, error: 'License key not found' };
|
||||
if (!existing) return { success: false, action: 'replace', error: 'License key not found' };
|
||||
|
||||
await prisma.licenseKey.delete({ where: { id: keyId } });
|
||||
|
||||
const hex = crypto.randomUUID().replace(/-/g, '').slice(0, 16).toUpperCase();
|
||||
const key = `${hex.slice(0, 4)}-${hex.slice(4, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}`;
|
||||
|
||||
await prisma.licenseKey.create({
|
||||
data: {
|
||||
key,
|
||||
key: generateLicenseKey(),
|
||||
active: true,
|
||||
level: existing.level,
|
||||
userId: existing.userId
|
||||
userId: existing.userId,
|
||||
email: existing.email
|
||||
}
|
||||
});
|
||||
|
||||
return { success: true, error: undefined };
|
||||
return { success: true, action: 'replace' };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,28 +1,90 @@
|
||||
<script lang="ts">
|
||||
import type { PageProps } from './$types';
|
||||
import { enhance } from '$app/forms';
|
||||
import AdminTable from '$lib/components/admin/AdminTable.svelte';
|
||||
import type { PageProps } from './$types';
|
||||
import DataTable, { type DataTableColumn } from '$lib/components/DataTable.svelte';
|
||||
import AdminModal from '$lib/components/admin/AdminModal.svelte';
|
||||
import Toast from '$lib/components/admin/Toast.svelte';
|
||||
import { getModelConfig } from '$lib/svelte-admin/registry';
|
||||
import LicenseKeyForm from './LicenseKeyForm.svelte';
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
type ActionResult = {
|
||||
success?: boolean;
|
||||
action?: string;
|
||||
error?: string;
|
||||
} | null;
|
||||
|
||||
const config = getModelConfig('license-keys')!;
|
||||
let { data, form }: PageProps = $props();
|
||||
|
||||
const columns: DataTableColumn[] = [
|
||||
{ label: 'Key', value: (r) => r.key },
|
||||
{
|
||||
label: 'User',
|
||||
value: (r) => (r.user as Record<string, unknown> | undefined)?.name ?? ''
|
||||
},
|
||||
{
|
||||
label: 'Email',
|
||||
value: (r) => (r.user as Record<string, unknown> | undefined)?.email ?? r.email ?? ''
|
||||
},
|
||||
{ label: 'Active', value: (r) => (r.active ? 'Yes' : 'No') }
|
||||
];
|
||||
|
||||
let createOpen = $state(false);
|
||||
let replaceOpen = $state(false);
|
||||
let replaceRecord = $state<Record<string, unknown> | null>(null);
|
||||
let toastMessage = $state('');
|
||||
let toastType = $state<'success' | 'error'>('success');
|
||||
|
||||
let email = $state('');
|
||||
let createError = $state('');
|
||||
let createSuccess = $state('');
|
||||
let replaceError = $state('');
|
||||
let replaceSuccess = $state('');
|
||||
|
||||
let lastCreateForm = $state<ActionResult>(null);
|
||||
let lastReplaceForm = $state<ActionResult>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (!form) return;
|
||||
|
||||
if (form.action === 'createKey') {
|
||||
if (form === lastCreateForm) return;
|
||||
lastCreateForm = form;
|
||||
if (form.success) {
|
||||
email = '';
|
||||
createError = '';
|
||||
createSuccess = 'License key created and emailed';
|
||||
setTimeout(() => {
|
||||
createOpen = false;
|
||||
createSuccess = '';
|
||||
}, 1200);
|
||||
} else {
|
||||
createSuccess = '';
|
||||
createError = form.error ?? '';
|
||||
}
|
||||
} else if (form.action === 'replace') {
|
||||
if (form === lastReplaceForm) return;
|
||||
lastReplaceForm = form;
|
||||
if (form.success) {
|
||||
replaceError = '';
|
||||
replaceSuccess = 'License key replaced';
|
||||
setTimeout(() => {
|
||||
replaceOpen = false;
|
||||
replaceSuccess = '';
|
||||
}, 1200);
|
||||
} else {
|
||||
replaceSuccess = '';
|
||||
replaceError = form.error ?? '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function openCreate() {
|
||||
lastCreateForm = null;
|
||||
createError = '';
|
||||
createSuccess = '';
|
||||
createOpen = true;
|
||||
}
|
||||
|
||||
function openReplace(record: Record<string, unknown>) {
|
||||
replaceRecord = record;
|
||||
lastReplaceForm = null;
|
||||
replaceError = '';
|
||||
replaceSuccess = '';
|
||||
replaceOpen = true;
|
||||
}
|
||||
|
||||
@@ -34,98 +96,111 @@
|
||||
replaceOpen = false;
|
||||
replaceRecord = null;
|
||||
}
|
||||
|
||||
function replaceEnhance() {
|
||||
return async ({ result, update }: any) => {
|
||||
if (result.type === 'success' && result.status === 200) {
|
||||
closeReplace();
|
||||
toastMessage = 'License key replaced';
|
||||
toastType = 'success';
|
||||
} else {
|
||||
toastMessage = 'Failed to replace license key';
|
||||
toastType = 'error';
|
||||
}
|
||||
await update();
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h1 class="text-xl font-bold text-gray-900 dark:text-gray-100">{config.displayName}</h1>
|
||||
<button
|
||||
type="button"
|
||||
onclick={openCreate}
|
||||
class="border border-gray-300 px-3 py-1.5 text-sm font-medium dark:border-gray-600 dark:text-gray-300"
|
||||
>
|
||||
New License Key +
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<AdminTable
|
||||
<div>
|
||||
<DataTable
|
||||
title="License Keys"
|
||||
{columns}
|
||||
records={data.records}
|
||||
listFields={config.listFields}
|
||||
searchFields={config.searchFields}
|
||||
fieldTypes={config.fieldTypes}
|
||||
singular={config.displayNameSingular}
|
||||
newLabel="New License Key"
|
||||
searchPlaceholder="Search license keys..."
|
||||
copyableFields={['key']}
|
||||
onRowClick={openReplace}
|
||||
onNew={openCreate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AdminModal title="Create License Key" open={createOpen} onClose={closeCreate}>
|
||||
<LicenseKeyForm subscriptionLevels={data.subscriptionLevels} onSuccess={closeCreate} />
|
||||
{#if createSuccess}
|
||||
<div
|
||||
class="mb-4 rounded-md border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700"
|
||||
>
|
||||
{createSuccess}
|
||||
</div>
|
||||
{/if}
|
||||
{#if createError}
|
||||
<div class="mb-4 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{createError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form method="POST" action="?/createKey" use:enhance class="flex flex-col gap-4">
|
||||
<div>
|
||||
<label for="field-email" class="mb-1 block text-sm font-medium text-slate-700">Email</label>
|
||||
<input
|
||||
id="field-email"
|
||||
type="email"
|
||||
name="email"
|
||||
required
|
||||
bind:value={email}
|
||||
placeholder="user@example.com"
|
||||
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-indigo-500 focus:outline-none"
|
||||
/>
|
||||
<p class="mt-1 text-sm text-slate-500">
|
||||
If no account exists for this email yet, the key will be attached when they register.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onclick={closeCreate}
|
||||
class="rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-indigo-700"
|
||||
>
|
||||
Create & Email Key
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AdminModal>
|
||||
|
||||
<AdminModal title="Replace License Key" open={replaceOpen} onClose={closeReplace}>
|
||||
{#if replaceSuccess}
|
||||
<div
|
||||
class="mb-4 rounded-md border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700"
|
||||
>
|
||||
{replaceSuccess}
|
||||
</div>
|
||||
{/if}
|
||||
{#if replaceError}
|
||||
<div class="mb-4 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{replaceError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if replaceRecord}
|
||||
<div class="flex flex-col gap-4">
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300">
|
||||
<p class="text-sm text-slate-700">
|
||||
Generate a new key to replace <strong>{String(replaceRecord.key)}</strong>?
|
||||
</p>
|
||||
<div
|
||||
class="rounded border border-gray-200 bg-gray-50 p-3 text-sm dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="mb-1 text-gray-500 dark:text-gray-400">
|
||||
User: <span class="font-medium text-gray-700 dark:text-gray-300"
|
||||
>{String(replaceRecord['user.name'] ?? '')}</span
|
||||
>
|
||||
</div>
|
||||
<div class="text-gray-500 dark:text-gray-400">
|
||||
Level: <span class="font-medium text-gray-700 dark:text-gray-300"
|
||||
>{String(replaceRecord.level ?? '')}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
The existing key will be deleted and a new one with the same subscription level will be
|
||||
created and emailed.
|
||||
<p class="text-sm text-slate-500">
|
||||
The existing key will be deleted and a new one will be created.
|
||||
</p>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/replace"
|
||||
use:enhance={replaceEnhance}
|
||||
class="flex justify-end gap-2 pt-2"
|
||||
>
|
||||
|
||||
<form method="POST" action="?/replace" use:enhance>
|
||||
<input type="hidden" name="id" value={String(replaceRecord.id)} />
|
||||
<button
|
||||
type="button"
|
||||
onclick={closeReplace}
|
||||
class="border border-gray-300 px-3 py-1.5 text-sm dark:border-gray-600 dark:text-gray-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="border border-amber-300 bg-amber-50 px-4 py-1.5 text-sm font-medium hover:bg-amber-100 dark:border-amber-700 dark:bg-amber-900/30 dark:text-amber-300 dark:hover:bg-amber-900/50"
|
||||
>
|
||||
Generate Replacement
|
||||
</button>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onclick={closeReplace}
|
||||
class="rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-red-700"
|
||||
>
|
||||
Generate Replacement
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
</AdminModal>
|
||||
|
||||
{#if toastMessage}
|
||||
<Toast message={toastMessage} type={toastType} />
|
||||
{/if}
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
|
||||
let {
|
||||
subscriptionLevels = [] as { label: string; value: string }[],
|
||||
onSuccess
|
||||
}: {
|
||||
subscriptionLevels?: { label: string; value: string }[];
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
let email = $state('');
|
||||
let level = $state('');
|
||||
let formError = $state('');
|
||||
|
||||
function handleEnhance() {
|
||||
return async ({ result, update }: any) => {
|
||||
if (result.type === 'success' && result.status === 200) {
|
||||
if (result.data?.error) {
|
||||
formError = result.data.error;
|
||||
} else {
|
||||
email = '';
|
||||
level = '';
|
||||
formError = '';
|
||||
await update();
|
||||
onSuccess?.();
|
||||
}
|
||||
} else {
|
||||
await update();
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<form method="POST" action="?/createKey" use:enhance={handleEnhance}>
|
||||
{#if formError}
|
||||
<div class="mb-4 border border-red-300 bg-red-50 px-4 py-2 text-sm text-red-700">
|
||||
{formError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<label
|
||||
for="field-email"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
User Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="field-email"
|
||||
name="email"
|
||||
required
|
||||
bind:value={email}
|
||||
class="w-full border border-gray-300 px-3 py-1.5 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
||||
placeholder="user@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="field-level"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Subscription Level
|
||||
</label>
|
||||
<select
|
||||
name="level"
|
||||
id="field-level"
|
||||
required
|
||||
onchange={(e) => (level = (e.target as HTMLSelectElement).value)}
|
||||
class="w-full border border-gray-300 px-3 py-1.5 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
||||
>
|
||||
<option value="" disabled selected={!level}>-- Select --</option>
|
||||
{#each subscriptionLevels as opt (opt.value)}
|
||||
<option value={opt.value} selected={opt.value === level}>{opt.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
class="border border-gray-300 bg-gray-100 px-4 py-1.5 text-sm font-medium hover:bg-gray-200 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
>
|
||||
Create & Email Key
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -8,38 +8,44 @@ const { sendMail } = vi.hoisted(() => ({
|
||||
|
||||
vi.mock('$lib/server/resend', () => ({ sendMail }));
|
||||
|
||||
vi.mock('$lib/svelte-admin/registry', () => ({
|
||||
getModelConfig: vi.fn(() => ({ slug: 'license-keys', listFields: [], searchFields: [] }))
|
||||
}));
|
||||
|
||||
vi.mock('$lib/svelte-admin/crud', () => ({
|
||||
loadRecords: vi.fn().mockResolvedValue({ records: [], relationOptionsData: {} })
|
||||
}));
|
||||
|
||||
vi.mock('$lib/db', () => ({
|
||||
prisma: {
|
||||
user: { findUnique: vi.fn() },
|
||||
profile: { findUnique: vi.fn() },
|
||||
licenseKey: { create: vi.fn() },
|
||||
subscriptionLevel: { findMany: vi.fn() }
|
||||
profile: { findFirst: vi.fn() },
|
||||
licenseKey: { findMany: vi.fn(), findUnique: vi.fn(), create: vi.fn(), delete: vi.fn() }
|
||||
}
|
||||
}));
|
||||
|
||||
import { prisma } from '$lib/db';
|
||||
import { actions } from './+page.server';
|
||||
import { load, actions } from './+page.server';
|
||||
|
||||
const mockFindUnique = prisma.user.findUnique as ReturnType<typeof vi.fn>;
|
||||
const mockProfileFindUnique = prisma.profile.findUnique as ReturnType<typeof vi.fn>;
|
||||
const mockProfileFindFirst = prisma.profile.findFirst as ReturnType<typeof vi.fn>;
|
||||
const mockLicenseKeyCreate = prisma.licenseKey.create as ReturnType<typeof vi.fn>;
|
||||
const mockLicenseKeyFindUnique = prisma.licenseKey.findUnique as ReturnType<typeof vi.fn>;
|
||||
const mockLicenseKeyDelete = prisma.licenseKey.delete as ReturnType<typeof vi.fn>;
|
||||
|
||||
describe('admin license-keys createKey action', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns records via load', async () => {
|
||||
const records = [{ id: 'k1', key: 'AAAA-BBBB-CCCC-DDDD' }];
|
||||
(prisma.licenseKey.findMany as ReturnType<typeof vi.fn>).mockResolvedValue(records);
|
||||
|
||||
const result = await load({} as any);
|
||||
|
||||
expect(prisma.licenseKey.findMany).toHaveBeenCalledWith({
|
||||
include: { user: true },
|
||||
orderBy: { key: 'asc' }
|
||||
});
|
||||
expect(result).toEqual({ records });
|
||||
});
|
||||
|
||||
it('rejects unauthenticated requests', async () => {
|
||||
const locals = createLocals();
|
||||
const req = createFormRequest({ email: 'jane@example.com', level: 'standard' });
|
||||
const req = createFormRequest({ email: 'jane@example.com' });
|
||||
|
||||
const err = await actions.createKey({ request: req, locals } as any).catch((e) => e);
|
||||
expect(isHttpError(err, 401)).toBe(true);
|
||||
@@ -47,27 +53,36 @@ describe('admin license-keys createKey action', () => {
|
||||
|
||||
it('returns error when email is missing', async () => {
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const req = createFormRequest({ level: 'standard' });
|
||||
const req = createFormRequest({});
|
||||
|
||||
const result = await actions.createKey({ request: req, locals } as any);
|
||||
expect(result).toEqual({ success: false, error: 'Email is required' });
|
||||
expect(result).toEqual({ success: false, action: 'createKey', error: 'Email is required' });
|
||||
});
|
||||
|
||||
it('returns error when level is missing', async () => {
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const req = createFormRequest({ email: 'jane@example.com' });
|
||||
|
||||
const result = await actions.createKey({ request: req, locals } as any);
|
||||
expect(result).toEqual({ success: false, error: 'Subscription level is required' });
|
||||
});
|
||||
|
||||
it('returns error when user is not found', async () => {
|
||||
it('creates a key with only an email when no user exists yet', async () => {
|
||||
mockFindUnique.mockResolvedValue(null);
|
||||
mockProfileFindFirst.mockResolvedValue(null);
|
||||
mockLicenseKeyCreate.mockResolvedValue({});
|
||||
sendMail.mockResolvedValue({ id: 'email-1' });
|
||||
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const req = createFormRequest({ email: 'nobody@example.com', level: 'standard' });
|
||||
const req = createFormRequest({ email: 'nobody@example.com' });
|
||||
|
||||
const result = await actions.createKey({ request: req, locals } as any);
|
||||
expect(result).toEqual({ success: false, error: 'No user found with that email' });
|
||||
|
||||
expect(result).toEqual({ success: true, action: 'createKey' });
|
||||
expect(mockLicenseKeyCreate).toHaveBeenCalledOnce();
|
||||
|
||||
const createCall = mockLicenseKeyCreate.mock.calls[0][0];
|
||||
expect(createCall.data.active).toBe(true);
|
||||
expect(createCall.data.userId).toBeUndefined();
|
||||
expect(createCall.data.email).toBe('nobody@example.com');
|
||||
|
||||
expect(sendMail).toHaveBeenCalledOnce();
|
||||
const mailCall = sendMail.mock.calls[0][0];
|
||||
expect(mailCall.to).toBe('nobody@example.com');
|
||||
expect(mailCall.templateId).toBe('license-key-delivery');
|
||||
expect(mailCall.data.firstName).toBe('Valued Customer');
|
||||
});
|
||||
|
||||
it('creates a license key and sends email with license_key variable', async () => {
|
||||
@@ -75,22 +90,21 @@ describe('admin license-keys createKey action', () => {
|
||||
const profile = { firstName: 'Jane' };
|
||||
|
||||
mockFindUnique.mockResolvedValue(user);
|
||||
mockProfileFindUnique.mockResolvedValue(profile);
|
||||
mockProfileFindFirst.mockResolvedValue(profile);
|
||||
mockLicenseKeyCreate.mockResolvedValue({});
|
||||
sendMail.mockResolvedValue({ id: 'email-1' });
|
||||
|
||||
const locals = createLocals(user as any);
|
||||
const req = createFormRequest({ email: user.email, level: 'standard' });
|
||||
const req = createFormRequest({ email: user.email });
|
||||
|
||||
const result = await actions.createKey({ request: req, locals } as any);
|
||||
|
||||
expect(result).toEqual({ success: true, error: undefined });
|
||||
expect(result).toEqual({ success: true, action: 'createKey' });
|
||||
expect(mockLicenseKeyCreate).toHaveBeenCalledOnce();
|
||||
|
||||
const createCall = mockLicenseKeyCreate.mock.calls[0][0];
|
||||
expect(createCall.data.key).toMatch(/^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/);
|
||||
expect(createCall.data.active).toBe(true);
|
||||
expect(createCall.data.level).toBe('standard');
|
||||
expect(createCall.data.userId).toBe('user-1');
|
||||
|
||||
expect(sendMail).toHaveBeenCalledOnce();
|
||||
@@ -106,12 +120,12 @@ describe('admin license-keys createKey action', () => {
|
||||
const user = mockUser();
|
||||
|
||||
mockFindUnique.mockResolvedValue(user);
|
||||
mockProfileFindUnique.mockResolvedValue(null);
|
||||
mockProfileFindFirst.mockResolvedValue(null);
|
||||
mockLicenseKeyCreate.mockResolvedValue({});
|
||||
sendMail.mockResolvedValue({ id: 'email-1' });
|
||||
|
||||
const locals = createLocals(user as any);
|
||||
const req = createFormRequest({ email: user.email, level: 'premium' });
|
||||
const req = createFormRequest({ email: user.email });
|
||||
|
||||
await actions.createKey({ request: req, locals } as any);
|
||||
|
||||
@@ -125,19 +139,76 @@ describe('admin license-keys createKey action', () => {
|
||||
const user = mockUser();
|
||||
|
||||
mockFindUnique.mockResolvedValue(user);
|
||||
mockProfileFindUnique.mockResolvedValue({ firstName: 'Jane' });
|
||||
mockProfileFindFirst.mockResolvedValue({ firstName: 'Jane' });
|
||||
mockLicenseKeyCreate.mockResolvedValue({});
|
||||
sendMail.mockRejectedValue(new Error('Resend API down'));
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
const locals = createLocals(user as any);
|
||||
const req = createFormRequest({ email: user.email, level: 'standard' });
|
||||
const req = createFormRequest({ email: user.email });
|
||||
|
||||
const result = await actions.createKey({ request: req, locals } as any);
|
||||
expect(result).toEqual({ success: true, error: undefined });
|
||||
expect(result).toEqual({ success: true, action: 'createKey' });
|
||||
expect(mockLicenseKeyCreate).toHaveBeenCalledOnce();
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('admin license-keys replace action', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('rejects unauthenticated requests', async () => {
|
||||
const locals = createLocals();
|
||||
const req = createFormRequest({ id: 'k1' });
|
||||
|
||||
const err = await actions.replace({ request: req, locals } as any).catch((e) => e);
|
||||
expect(isHttpError(err, 401)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns error when id is missing', async () => {
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const req = createFormRequest({});
|
||||
|
||||
const result = await actions.replace({ request: req, locals } as any);
|
||||
expect(result).toEqual({ success: false, action: 'replace', error: 'Missing key id' });
|
||||
});
|
||||
|
||||
it('returns error when the key does not exist', async () => {
|
||||
mockLicenseKeyFindUnique.mockResolvedValue(null);
|
||||
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const req = createFormRequest({ id: 'missing' });
|
||||
|
||||
const result = await actions.replace({ request: req, locals } as any);
|
||||
expect(result).toEqual({ success: false, action: 'replace', error: 'License key not found' });
|
||||
});
|
||||
|
||||
it('deletes the old key and creates a replacement preserving the owner', async () => {
|
||||
mockLicenseKeyFindUnique.mockResolvedValue({
|
||||
id: 'k1',
|
||||
key: 'AAAA-BBBB-CCCC-DDDD',
|
||||
userId: 'user-1',
|
||||
email: null
|
||||
});
|
||||
mockLicenseKeyDelete.mockResolvedValue({});
|
||||
mockLicenseKeyCreate.mockResolvedValue({});
|
||||
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const req = createFormRequest({ id: 'k1' });
|
||||
|
||||
const result = await actions.replace({ request: req, locals } as any);
|
||||
|
||||
expect(result).toEqual({ success: true, action: 'replace' });
|
||||
expect(mockLicenseKeyDelete).toHaveBeenCalledWith({ where: { id: 'k1' } });
|
||||
expect(mockLicenseKeyCreate).toHaveBeenCalledOnce();
|
||||
const createCall = mockLicenseKeyCreate.mock.calls[0][0];
|
||||
expect(createCall.data.key).toMatch(/^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/);
|
||||
expect(createCall.data.active).toBe(true);
|
||||
expect(createCall.data.userId).toBe('user-1');
|
||||
expect(createCall.data.email).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { getSerializableModelConfigs } from '$lib/svelte-admin/registry';
|
||||
import { createLocals, mockUser } from '$lib/test-utils';
|
||||
|
||||
vi.mock('$lib/db', () => ({}));
|
||||
|
||||
import { load } from './+page.server';
|
||||
|
||||
function localsWithRole(roleId: number) {
|
||||
return createLocals(mockUser({ roleId }) as any);
|
||||
}
|
||||
|
||||
describe('getSerializableModelConfigs', () => {
|
||||
it('returns only serializable fields (no functions)', () => {
|
||||
const models = getSerializableModelConfigs(1);
|
||||
const serialized = JSON.stringify(models);
|
||||
expect(serialized).toBeDefined();
|
||||
|
||||
for (const model of models) {
|
||||
expect(typeof model.slug).toBe('string');
|
||||
expect(typeof model.displayName).toBe('string');
|
||||
expect(Array.isArray(model.listFields)).toBe(true);
|
||||
expect(
|
||||
typeof model.accessFilterResult === 'object' || model.accessFilterResult === undefined
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('evaluates accessFilter for SUPER (roleId 1) — returns undefined for users', () => {
|
||||
const models = getSerializableModelConfigs(1);
|
||||
const users = models.find((m) => m.slug === 'users')!;
|
||||
expect(users.accessFilterResult).toBeUndefined();
|
||||
});
|
||||
|
||||
it('evaluates accessFilter for ADMIN (roleId 2) — restricts users to STAFF+', () => {
|
||||
const models = getSerializableModelConfigs(2);
|
||||
const users = models.find((m) => m.slug === 'users')!;
|
||||
expect(users.accessFilterResult).toEqual({ roleId: { gte: 3 } });
|
||||
});
|
||||
|
||||
it('evaluates accessFilter for STAFF (roleId 3) — restricts users to USER-only', () => {
|
||||
const models = getSerializableModelConfigs(3);
|
||||
const users = models.find((m) => m.slug === 'users')!;
|
||||
expect(users.accessFilterResult).toEqual({ roleId: { gt: 3 } });
|
||||
});
|
||||
|
||||
it('models without accessFilter have undefined accessFilterResult', () => {
|
||||
const models = getSerializableModelConfigs(1);
|
||||
const roles = models.find((m) => m.slug === 'user-roles')!;
|
||||
expect(roles.accessFilterResult).toBeUndefined();
|
||||
});
|
||||
|
||||
it('excludes specified slugs', () => {
|
||||
const models = getSerializableModelConfigs(1, ['license-keys', 'users']);
|
||||
expect(models.find((m) => m.slug === 'license-keys')).toBeUndefined();
|
||||
expect(models.find((m) => m.slug === 'users')).toBeUndefined();
|
||||
expect(models.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('admin index page load', () => {
|
||||
it('returns models with evaluated accessFilter results', async () => {
|
||||
const result = (await load({ locals: localsWithRole(1) } as any)) as any;
|
||||
expect(result.models).toBeDefined();
|
||||
expect(Array.isArray(result.models)).toBe(true);
|
||||
|
||||
const users = result.models.find((m: any) => m.slug === 'users');
|
||||
expect(users).toBeDefined();
|
||||
expect(users.accessFilterResult).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns different accessFilter results per role', async () => {
|
||||
const superResult = (await load({ locals: localsWithRole(1) } as any)) as any;
|
||||
const adminResult = (await load({ locals: localsWithRole(2) } as any)) as any;
|
||||
const staffResult = (await load({ locals: localsWithRole(3) } as any)) as any;
|
||||
|
||||
const superUsers = superResult.models.find((m: any) => m.slug === 'users');
|
||||
const adminUsers = adminResult.models.find((m: any) => m.slug === 'users');
|
||||
const staffUsers = staffResult.models.find((m: any) => m.slug === 'users');
|
||||
|
||||
expect(superUsers.accessFilterResult).toBeUndefined();
|
||||
expect(adminUsers.accessFilterResult).toEqual({ roleId: { gte: 3 } });
|
||||
expect(staffUsers.accessFilterResult).toEqual({ roleId: { gt: 3 } });
|
||||
});
|
||||
|
||||
it('excludes license-keys model', async () => {
|
||||
const result = (await load({ locals: localsWithRole(1) } as any)) as any;
|
||||
const licenseKeys = result.models.find((m: any) => m.slug === 'license-keys');
|
||||
expect(licenseKeys).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns fully JSON-serializable data (no functions)', async () => {
|
||||
const result = (await load({ locals: localsWithRole(2) } as any)) as any;
|
||||
const serialized = JSON.stringify(result);
|
||||
const parsed = JSON.parse(serialized);
|
||||
expect(parsed.models).toBeDefined();
|
||||
expect(parsed.models.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('defaults to roleId 4 when user has no roleId', async () => {
|
||||
const result = (await load({
|
||||
locals: createLocals(mockUser({ roleId: null }) as any)
|
||||
} as any)) as any;
|
||||
const users = result.models.find((m: any) => m.slug === 'users');
|
||||
expect(users.accessFilterResult).toEqual({ roleId: { gt: 3 } });
|
||||
});
|
||||
});
|
||||
@@ -1,146 +1,81 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getModelConfig } from '$lib/svelte-admin/registry';
|
||||
import { getDelegate } from '$lib/svelte-admin/prisma-delegate';
|
||||
import { prisma } from '$lib/db';
|
||||
import { superValidate } from 'sveltekit-superforms/server';
|
||||
import { zod4 } from 'sveltekit-superforms/adapters';
|
||||
import { z } from 'zod';
|
||||
import { superFormErrorsToFieldErrors } from '$lib/superform-errors';
|
||||
|
||||
const config = getModelConfig('proclamation-categories')!;
|
||||
const CategorySchema = z.object({
|
||||
name: z.string().min(1, 'Required').max(100, 'Too long')
|
||||
});
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
const delegate = getDelegate(config);
|
||||
|
||||
const records = await delegate.findMany({
|
||||
where: { parentId: null },
|
||||
include: config.include,
|
||||
orderBy: { sortOrder: 'asc' }
|
||||
export const load: PageServerLoad = async () => {
|
||||
const records = await prisma.proclamationCategory.findMany({
|
||||
orderBy: [{ sortOrder: 'asc' }, { name: 'asc' }]
|
||||
});
|
||||
|
||||
const relationOptionsData: Record<string, { label: string; value: string | number }[]> = {};
|
||||
|
||||
for (const [field, relConfig] of Object.entries(config.relationOptions ?? {})) {
|
||||
const relDelegate = getDelegate({ ...config, modelName: relConfig.model });
|
||||
const selectFields: Record<string, boolean> = {
|
||||
[relConfig.valueField]: true,
|
||||
[relConfig.labelField]: true
|
||||
};
|
||||
const options = await relDelegate.findMany({
|
||||
select: selectFields,
|
||||
orderBy: { [relConfig.labelField]: 'asc' }
|
||||
});
|
||||
const mapped = (options as Record<string, unknown>[]).map((opt: Record<string, unknown>) => ({
|
||||
label: String(opt[relConfig.labelField]),
|
||||
value: opt[relConfig.valueField] as string | number
|
||||
}));
|
||||
relationOptionsData[field] = mapped;
|
||||
}
|
||||
|
||||
return {
|
||||
records: records as Record<string, unknown>[],
|
||||
relationOptionsData
|
||||
};
|
||||
return { records: records as unknown as Record<string, unknown>[] };
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
upsert: async ({ request, locals }) => {
|
||||
upsert: async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
|
||||
const name = (formData.get('name')?.toString() ?? '').trim();
|
||||
const rawId = formData.get('id')?.toString();
|
||||
const childIds = formData.getAll('childId').map((v) => v.toString());
|
||||
const childNames = formData
|
||||
.getAll('childName')
|
||||
.map((v) => v.toString().trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (!name) {
|
||||
return { success: false, error: 'Name is required', fieldErrors: { name: 'Required' } };
|
||||
const form = await superValidate(formData, zod4(CategorySchema));
|
||||
if (!form.valid) {
|
||||
const { error: formError, fieldErrors } = superFormErrorsToFieldErrors(
|
||||
form.errors as Record<string, unknown>
|
||||
);
|
||||
return { success: false, action: 'upsert', error: formError, fieldErrors };
|
||||
}
|
||||
|
||||
const delegate = getDelegate(config);
|
||||
const rawId = formData.get('id')?.toString();
|
||||
const name = form.data.name;
|
||||
|
||||
try {
|
||||
let categoryId: number;
|
||||
if (rawId) {
|
||||
const id = parseInt(rawId, 10);
|
||||
await delegate.update({
|
||||
where: { id },
|
||||
await prisma.proclamationCategory.update({
|
||||
where: { id: Number(rawId) },
|
||||
data: { name }
|
||||
});
|
||||
categoryId = id;
|
||||
} else {
|
||||
const maxSort = await delegate.findFirst({
|
||||
where: { parentId: null },
|
||||
const maxSort = await prisma.proclamationCategory.findFirst({
|
||||
orderBy: { sortOrder: 'desc' },
|
||||
select: { sortOrder: true }
|
||||
});
|
||||
const sortOrder = (maxSort?.sortOrder ?? -1) + 1;
|
||||
const created = await delegate.create({
|
||||
data: { name, sortOrder }
|
||||
await prisma.proclamationCategory.create({
|
||||
data: { name, sortOrder: (maxSort?.sortOrder ?? -1) + 1 }
|
||||
});
|
||||
categoryId = created.id;
|
||||
}
|
||||
|
||||
const existingChildren = await delegate.findMany({
|
||||
where: { parentId: categoryId },
|
||||
select: { id: true }
|
||||
});
|
||||
const existingChildIdSet = new Set(existingChildren.map((c: { id: number }) => c.id));
|
||||
|
||||
const submittedIdSet = new Set<number>();
|
||||
|
||||
for (let i = 0; i < childNames.length; i++) {
|
||||
const childName = childNames[i];
|
||||
if (!childName) continue;
|
||||
|
||||
const childIdRaw = childIds[i] ? parseInt(childIds[i], 10) : null;
|
||||
|
||||
if (childIdRaw && existingChildIdSet.has(childIdRaw)) {
|
||||
submittedIdSet.add(childIdRaw);
|
||||
await delegate.update({
|
||||
where: { id: childIdRaw },
|
||||
data: { name: childName }
|
||||
});
|
||||
} else {
|
||||
await delegate.create({
|
||||
data: { name: childName, parentId: categoryId }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const child of existingChildren) {
|
||||
if (!submittedIdSet.has(child.id)) {
|
||||
await delegate.delete({ where: { id: child.id } });
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (e: unknown) {
|
||||
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2002') {
|
||||
return {
|
||||
success: false,
|
||||
error: 'A sub-category with this name already exists under this parent.'
|
||||
action: 'upsert',
|
||||
error: 'A category with this name already exists.'
|
||||
};
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
return { success: true, action: 'upsert' };
|
||||
},
|
||||
delete: async ({ request, locals }) => {
|
||||
delete: async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
const rawId = formData.get('id')?.toString();
|
||||
if (!rawId) return { success: false, error: 'Missing id' };
|
||||
|
||||
const id = parseInt(rawId, 10);
|
||||
const delegate = getDelegate(config);
|
||||
|
||||
const viewerRoleId = (locals.user as { roleId?: number } | null)?.roleId ?? 4;
|
||||
const where = config.accessFilter?.(viewerRoleId);
|
||||
if (where) {
|
||||
const match = await delegate.findFirst({ where: { id, ...where } });
|
||||
if (!match) return { success: false, error: 'Access denied' };
|
||||
try {
|
||||
await prisma.proclamationCategory.delete({ where: { id: Number(rawId) } });
|
||||
} catch (e: unknown) {
|
||||
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2003') {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Cannot delete: one or more proclamations use this category.'
|
||||
};
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
await delegate.deleteMany({ where: { parentId: id } });
|
||||
await delegate.delete({ where: { id } });
|
||||
|
||||
return { success: true };
|
||||
return { success: true, action: 'delete' };
|
||||
},
|
||||
reorder: async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
@@ -148,16 +83,13 @@ export const actions = {
|
||||
if (!raw) return { success: false, error: 'Missing ids' };
|
||||
|
||||
const ids = JSON.parse(raw) as number[];
|
||||
|
||||
const delegate = getDelegate(config);
|
||||
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
await delegate.update({
|
||||
await prisma.proclamationCategory.update({
|
||||
where: { id: ids[i] },
|
||||
data: { sortOrder: i }
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
return { success: true, action: 'reorder' };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,36 +1,57 @@
|
||||
<script lang="ts">
|
||||
import type { PageProps } from './$types';
|
||||
import type { ActionResult } from '$lib/svelte-admin/types';
|
||||
import AdminTable from '$lib/components/admin/AdminTable.svelte';
|
||||
import AdminModal from '$lib/components/admin/AdminModal.svelte';
|
||||
import Toast from '$lib/components/admin/Toast.svelte';
|
||||
import { getModelConfig } from '$lib/svelte-admin/registry';
|
||||
import { enhance } from '$app/forms';
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import CategoryForm from './CategoryForm.svelte';
|
||||
import type { PageProps } from './$types';
|
||||
import DataTable, { type DataTableColumn } from '$lib/components/DataTable.svelte';
|
||||
import AdminModal from '$lib/components/admin/AdminModal.svelte';
|
||||
|
||||
type ActionResult = {
|
||||
success?: boolean;
|
||||
action?: string;
|
||||
error?: string;
|
||||
fieldErrors?: Record<string, string>;
|
||||
} | null;
|
||||
|
||||
let { data, form }: PageProps = $props();
|
||||
let canDelete = $derived((data.adminUser?.roleId ?? 4) <= 2);
|
||||
|
||||
const config = getModelConfig('proclamation-categories')!;
|
||||
const canDelete = $derived((data.adminUser?.roleId ?? 4) <= 2);
|
||||
const columns: DataTableColumn[] = [{ label: 'Name', value: (r) => r.name }];
|
||||
|
||||
let modalOpen = $state(false);
|
||||
let editingRecord = $state<Record<string, unknown> | null>(null);
|
||||
let toastMessage = $state('');
|
||||
let toastType = $state<'success' | 'error'>('success');
|
||||
let fieldErrors = $state<Record<string, string>>({});
|
||||
|
||||
let name = $state('');
|
||||
let formError = $state('');
|
||||
let fieldErrors = $state<Record<string, string>>({});
|
||||
let successMessage = $state('');
|
||||
let lastForm = $state<ActionResult>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (form && 'success' in (form as any)) {
|
||||
const result = form as any;
|
||||
if (result.success) {
|
||||
toastMessage = 'Saved successfully';
|
||||
toastType = 'success';
|
||||
closeModal();
|
||||
} else {
|
||||
formError = result.error ?? '';
|
||||
fieldErrors = result.fieldErrors ?? {};
|
||||
}
|
||||
const rec = editingRecord ?? {};
|
||||
name = String(rec.name ?? '');
|
||||
if (!editingRecord) {
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
successMessage = '';
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!form || form === lastForm) return;
|
||||
lastForm = form;
|
||||
|
||||
if (form.success) {
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
successMessage = editingRecord ? 'Category updated' : 'Category created';
|
||||
setTimeout(() => {
|
||||
modalOpen = false;
|
||||
successMessage = '';
|
||||
}, 1200);
|
||||
} else {
|
||||
successMessage = '';
|
||||
formError = form.error ?? '';
|
||||
fieldErrors = form.fieldErrors ?? {};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -43,34 +64,29 @@
|
||||
|
||||
function openCreate() {
|
||||
editingRecord = null;
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
lastForm = null;
|
||||
modalOpen = true;
|
||||
}
|
||||
|
||||
function openEdit(record: Record<string, unknown>) {
|
||||
editingRecord = record;
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
lastForm = null;
|
||||
modalOpen = true;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modalOpen = false;
|
||||
editingRecord = null;
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<h1 class="mb-4 text-xl font-bold text-gray-900 dark:text-gray-100">{config.displayName}</h1>
|
||||
<AdminTable
|
||||
<div>
|
||||
<DataTable
|
||||
title="Proclamation Categories"
|
||||
{columns}
|
||||
records={data.records}
|
||||
listFields={config.listFields}
|
||||
searchFields={config.searchFields}
|
||||
fieldTypes={config.fieldTypes}
|
||||
singular={config.displayNameSingular}
|
||||
newLabel="New Category"
|
||||
searchPlaceholder="Search categories..."
|
||||
draggable={true}
|
||||
onReorder={handleReorder}
|
||||
onRowClick={openEdit}
|
||||
@@ -78,23 +94,62 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AdminModal
|
||||
title={editingRecord
|
||||
? `Edit ${config.displayNameSingular}`
|
||||
: `Create ${config.displayNameSingular}`}
|
||||
open={modalOpen}
|
||||
onClose={closeModal}
|
||||
{canDelete}
|
||||
recordId={editingRecord?.id as string | undefined}
|
||||
>
|
||||
<CategoryForm
|
||||
record={editingRecord ?? {}}
|
||||
{fieldErrors}
|
||||
{formError}
|
||||
submitLabel={editingRecord ? 'Save' : 'Create'}
|
||||
/>
|
||||
</AdminModal>
|
||||
{#if modalOpen}
|
||||
<AdminModal
|
||||
title={editingRecord ? 'Edit Category' : 'Create Category'}
|
||||
open={modalOpen}
|
||||
onClose={closeModal}
|
||||
{canDelete}
|
||||
recordId={editingRecord?.id as string | undefined}
|
||||
>
|
||||
{#if successMessage}
|
||||
<div
|
||||
class="mb-4 rounded-md border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700"
|
||||
>
|
||||
{successMessage}
|
||||
</div>
|
||||
{/if}
|
||||
{#if formError}
|
||||
<div class="mb-4 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{formError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if toastMessage}
|
||||
<Toast message={toastMessage} type={toastType} />
|
||||
<form method="POST" action="?/upsert" use:enhance class="flex flex-col gap-4">
|
||||
{#if editingRecord}
|
||||
<input type="hidden" name="id" value={String(editingRecord.id)} />
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<label for="field-name" class="mb-1 block text-sm font-medium text-slate-700">Name</label>
|
||||
<input
|
||||
id="field-name"
|
||||
type="text"
|
||||
name="name"
|
||||
required
|
||||
bind:value={name}
|
||||
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-indigo-500 focus:outline-none"
|
||||
/>
|
||||
{#if fieldErrors.name}
|
||||
<p class="mt-1 text-sm text-red-600">{fieldErrors.name}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onclick={closeModal}
|
||||
class="rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-indigo-700"
|
||||
>
|
||||
{editingRecord ? 'Save Category' : 'Create Category'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AdminModal>
|
||||
{/if}
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
|
||||
let {
|
||||
record = {} as Record<string, unknown>,
|
||||
fieldErrors = {} as Record<string, string>,
|
||||
formError = '',
|
||||
action = '?/upsert',
|
||||
submitLabel = 'Save'
|
||||
}: {
|
||||
record?: Record<string, unknown>;
|
||||
fieldErrors?: Record<string, string>;
|
||||
formError?: string;
|
||||
action?: string;
|
||||
submitLabel?: string;
|
||||
} = $props();
|
||||
|
||||
let name = $state('');
|
||||
let children = $state<{ id: number | null; name: string }[]>([]);
|
||||
let dirty = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
dirty = false;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
name = String(record.name ?? '');
|
||||
const existingChildren = (record.children as Array<{ id: number; name: string }>) ?? [];
|
||||
children = existingChildren.map((c) => ({ id: c.id, name: c.name }));
|
||||
});
|
||||
|
||||
function markDirty() {
|
||||
if (record.id) dirty = true;
|
||||
}
|
||||
|
||||
function addChild() {
|
||||
children = [...children, { id: null, name: '' }];
|
||||
if (record.id) dirty = true;
|
||||
}
|
||||
|
||||
function removeChild(index: number) {
|
||||
children = children.filter((_, i) => i !== index);
|
||||
if (record.id) dirty = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<form {action} method="POST" use:enhance oninput={markDirty}>
|
||||
{#if record?.id}
|
||||
<input type="hidden" name="id" value={String(record.id)} />
|
||||
{/if}
|
||||
|
||||
{#if formError}
|
||||
<div class="mb-4 border border-red-300 bg-red-50 px-4 py-2 text-sm text-red-700">
|
||||
{formError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<label
|
||||
for="field-name"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="field-name"
|
||||
name="name"
|
||||
bind:value={name}
|
||||
class="w-full border px-3 py-1.5 text-sm {fieldErrors.name
|
||||
? 'border-red-400'
|
||||
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
{#if fieldErrors.name}
|
||||
<p class="mt-1 text-xs text-red-600">{fieldErrors.name}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Sub-categories
|
||||
</label>
|
||||
{#each children as child, i}
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<input type="hidden" name="childId" value={child.id ?? ''} />
|
||||
<input
|
||||
type="text"
|
||||
name="childName"
|
||||
bind:value={child.name}
|
||||
placeholder="Sub-category name"
|
||||
class="flex-1 border px-3 py-1.5 text-sm {fieldErrors[`childName.${i}`]
|
||||
? 'border-red-400'
|
||||
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => removeChild(i)}
|
||||
class="shrink-0 px-2 py-1 text-sm text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
<button
|
||||
type="button"
|
||||
onclick={addChild}
|
||||
class="mt-1 text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
+ Add Sub-category
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!!record?.id && !dirty}
|
||||
class="border border-gray-300 bg-gray-100 px-4 py-1.5 text-sm font-medium hover:bg-gray-200 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
>
|
||||
{submitLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,95 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { prisma } from '$lib/db';
|
||||
import { superValidate } from 'sveltekit-superforms/server';
|
||||
import { zod4 } from 'sveltekit-superforms/adapters';
|
||||
import { z } from 'zod';
|
||||
import { superFormErrorsToFieldErrors } from '$lib/superform-errors';
|
||||
|
||||
const GroupSchema = z.object({
|
||||
name: z.string().min(1, 'Required').max(100, 'Too long')
|
||||
});
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
const records = await prisma.proclamationGroup.findMany({
|
||||
orderBy: [{ sortOrder: 'asc' }, { name: 'asc' }]
|
||||
});
|
||||
return { records: records as unknown as Record<string, unknown>[] };
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
upsert: async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
const form = await superValidate(formData, zod4(GroupSchema));
|
||||
if (!form.valid) {
|
||||
const { error: formError, fieldErrors } = superFormErrorsToFieldErrors(
|
||||
form.errors as Record<string, unknown>
|
||||
);
|
||||
return { success: false, action: 'upsert', error: formError, fieldErrors };
|
||||
}
|
||||
|
||||
const rawId = formData.get('id')?.toString();
|
||||
const name = form.data.name;
|
||||
|
||||
try {
|
||||
if (rawId) {
|
||||
await prisma.proclamationGroup.update({
|
||||
where: { id: Number(rawId) },
|
||||
data: { name }
|
||||
});
|
||||
} else {
|
||||
const maxSort = await prisma.proclamationGroup.findFirst({
|
||||
orderBy: { sortOrder: 'desc' },
|
||||
select: { sortOrder: true }
|
||||
});
|
||||
await prisma.proclamationGroup.create({
|
||||
data: { name, sortOrder: (maxSort?.sortOrder ?? -1) + 1 }
|
||||
});
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2002') {
|
||||
return {
|
||||
success: false,
|
||||
action: 'upsert',
|
||||
error: 'A group with this name already exists.'
|
||||
};
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
return { success: true, action: 'upsert' };
|
||||
},
|
||||
delete: async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
const rawId = formData.get('id')?.toString();
|
||||
if (!rawId) return { success: false, error: 'Missing id' };
|
||||
|
||||
try {
|
||||
await prisma.proclamationGroup.delete({ where: { id: Number(rawId) } });
|
||||
} catch (e: unknown) {
|
||||
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2003') {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Cannot delete: one or more proclamations use this group.'
|
||||
};
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
return { success: true, action: 'delete' };
|
||||
},
|
||||
reorder: async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
const raw = formData.get('ids')?.toString();
|
||||
if (!raw) return { success: false, error: 'Missing ids' };
|
||||
|
||||
const ids = JSON.parse(raw) as number[];
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
await prisma.proclamationGroup.update({
|
||||
where: { id: ids[i] },
|
||||
data: { sortOrder: i }
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true, action: 'reorder' };
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,155 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import type { PageProps } from './$types';
|
||||
import DataTable, { type DataTableColumn } from '$lib/components/DataTable.svelte';
|
||||
import AdminModal from '$lib/components/admin/AdminModal.svelte';
|
||||
|
||||
type ActionResult = {
|
||||
success?: boolean;
|
||||
action?: string;
|
||||
error?: string;
|
||||
fieldErrors?: Record<string, string>;
|
||||
} | null;
|
||||
|
||||
let { data, form }: PageProps = $props();
|
||||
let canDelete = $derived((data.adminUser?.roleId ?? 4) <= 2);
|
||||
|
||||
const columns: DataTableColumn[] = [{ label: 'Name', value: (r) => r.name }];
|
||||
|
||||
let modalOpen = $state(false);
|
||||
let editingRecord = $state<Record<string, unknown> | null>(null);
|
||||
|
||||
let name = $state('');
|
||||
let formError = $state('');
|
||||
let fieldErrors = $state<Record<string, string>>({});
|
||||
let successMessage = $state('');
|
||||
let lastForm = $state<ActionResult>(null);
|
||||
|
||||
$effect(() => {
|
||||
const rec = editingRecord ?? {};
|
||||
name = String(rec.name ?? '');
|
||||
if (!editingRecord) {
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
successMessage = '';
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!form || form === lastForm) return;
|
||||
lastForm = form;
|
||||
|
||||
if (form.success) {
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
successMessage = editingRecord ? 'Group updated' : 'Group created';
|
||||
setTimeout(() => {
|
||||
modalOpen = false;
|
||||
successMessage = '';
|
||||
}, 1200);
|
||||
} else {
|
||||
successMessage = '';
|
||||
formError = form.error ?? '';
|
||||
fieldErrors = form.fieldErrors ?? {};
|
||||
}
|
||||
});
|
||||
|
||||
async function handleReorder(ids: (string | number)[]) {
|
||||
const formData = new FormData();
|
||||
formData.set('ids', JSON.stringify(ids));
|
||||
await fetch('?/reorder', { method: 'POST', body: formData });
|
||||
await invalidateAll();
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editingRecord = null;
|
||||
lastForm = null;
|
||||
modalOpen = true;
|
||||
}
|
||||
|
||||
function openEdit(record: Record<string, unknown>) {
|
||||
editingRecord = record;
|
||||
lastForm = null;
|
||||
modalOpen = true;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modalOpen = false;
|
||||
editingRecord = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<DataTable
|
||||
title="Proclamation Groups"
|
||||
{columns}
|
||||
records={data.records}
|
||||
newLabel="New Group"
|
||||
searchPlaceholder="Search groups..."
|
||||
draggable={true}
|
||||
onReorder={handleReorder}
|
||||
onRowClick={openEdit}
|
||||
onNew={openCreate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if modalOpen}
|
||||
<AdminModal
|
||||
title={editingRecord ? 'Edit Group' : 'Create Group'}
|
||||
open={modalOpen}
|
||||
onClose={closeModal}
|
||||
{canDelete}
|
||||
recordId={editingRecord?.id as string | undefined}
|
||||
>
|
||||
{#if successMessage}
|
||||
<div
|
||||
class="mb-4 rounded-md border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700"
|
||||
>
|
||||
{successMessage}
|
||||
</div>
|
||||
{/if}
|
||||
{#if formError}
|
||||
<div class="mb-4 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{formError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form method="POST" action="?/upsert" use:enhance class="flex flex-col gap-4">
|
||||
{#if editingRecord}
|
||||
<input type="hidden" name="id" value={String(editingRecord.id)} />
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<label for="field-name" class="mb-1 block text-sm font-medium text-slate-700">Name</label>
|
||||
<input
|
||||
id="field-name"
|
||||
type="text"
|
||||
name="name"
|
||||
required
|
||||
bind:value={name}
|
||||
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-indigo-500 focus:outline-none"
|
||||
/>
|
||||
{#if fieldErrors.name}
|
||||
<p class="mt-1 text-sm text-red-600">{fieldErrors.name}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onclick={closeModal}
|
||||
class="rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-indigo-700"
|
||||
>
|
||||
{editingRecord ? 'Save Group' : 'Create Group'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AdminModal>
|
||||
{/if}
|
||||
@@ -1,78 +1,87 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getModelConfig } from '$lib/svelte-admin/registry';
|
||||
import { loadRecords, deleteRecord, superFormErrorsToFieldErrors } from '$lib/svelte-admin/crud';
|
||||
import { prisma } from '$lib/db';
|
||||
import { superValidate } from 'sveltekit-superforms/server';
|
||||
import { zod4 } from 'sveltekit-superforms/adapters';
|
||||
import * as Schemas from '$lib/svelte-admin/schemas';
|
||||
import { z } from 'zod';
|
||||
import { superFormErrorsToFieldErrors } from '$lib/superform-errors';
|
||||
|
||||
const config = getModelConfig('proclamations')!;
|
||||
|
||||
function viewerRoleId(locals: App.Locals): number {
|
||||
return (locals.user as { roleId?: number } | null)?.roleId ?? 4;
|
||||
}
|
||||
const ProclamationSchema = z.object({
|
||||
name: z.string().min(1, 'Required'),
|
||||
solfeggioFrequencyId: z.coerce.number().int(),
|
||||
categoryId: z.coerce.number().int(),
|
||||
groupId: z.coerce.number().int(),
|
||||
content: z.string().optional().default('')
|
||||
});
|
||||
|
||||
function isSuper(locals: App.Locals): boolean {
|
||||
return viewerRoleId(locals) <= 1;
|
||||
return ((locals.user as { roleId?: number } | null)?.roleId ?? 4) <= 1;
|
||||
}
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
const base = await loadRecords(locals, config);
|
||||
const [proclamations, frequencies, categories, groups] = await Promise.all([
|
||||
prisma.proclamation.findMany({
|
||||
include: { category: true, group: true, solfeggioFrequency: true },
|
||||
orderBy: { name: 'asc' }
|
||||
}),
|
||||
prisma.solfeggioFrequency.findMany({
|
||||
select: { id: true, name: true, value: true },
|
||||
orderBy: { name: 'asc' }
|
||||
}),
|
||||
prisma.proclamationCategory.findMany({
|
||||
select: { id: true, name: true },
|
||||
orderBy: { name: 'asc' }
|
||||
}),
|
||||
prisma.proclamationGroup.findMany({
|
||||
select: { id: true, name: true },
|
||||
orderBy: { sortOrder: 'asc' }
|
||||
})
|
||||
]);
|
||||
|
||||
const records = proclamations as unknown as Record<string, unknown>[];
|
||||
|
||||
// Strip content from non-Super users
|
||||
if (!isSuper(locals)) {
|
||||
for (const record of base.records) {
|
||||
for (const record of records) {
|
||||
delete record.content;
|
||||
}
|
||||
}
|
||||
|
||||
const categories = await prisma.proclamationCategory.findMany({
|
||||
select: { id: true, name: true, parentId: true },
|
||||
orderBy: { name: 'asc' }
|
||||
});
|
||||
|
||||
return { ...base, categories };
|
||||
return { records, frequencies, categories, groups };
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
upsert: async ({ request, locals }) => {
|
||||
if (!locals.user) {
|
||||
return { success: false, error: 'Unauthorized' };
|
||||
return { success: false, action: 'upsert', error: 'Unauthorized' };
|
||||
}
|
||||
|
||||
if (!isSuper(locals)) {
|
||||
return { success: false, error: 'Only Super admins can edit proclamations' };
|
||||
return {
|
||||
success: false,
|
||||
action: 'upsert',
|
||||
error: 'Only Super admins can edit proclamations'
|
||||
};
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const form = await superValidate(formData, zod4(Schemas.Proclamation));
|
||||
const form = await superValidate(formData, zod4(ProclamationSchema));
|
||||
if (!form.valid) {
|
||||
const { error, fieldErrors } = superFormErrorsToFieldErrors(
|
||||
const { error: formError, fieldErrors } = superFormErrorsToFieldErrors(
|
||||
form.errors as Record<string, unknown>
|
||||
);
|
||||
return { success: false, error, fieldErrors };
|
||||
return { success: false, action: 'upsert', error: formError, fieldErrors };
|
||||
}
|
||||
|
||||
const rawId = formData.get('id')?.toString();
|
||||
const { parentCategoryId, ...rest } = form.data;
|
||||
|
||||
const data: Record<string, unknown> = {
|
||||
...rest,
|
||||
categoryId: rest.categoryId ?? parentCategoryId
|
||||
};
|
||||
const data: Record<string, unknown> = { ...form.data };
|
||||
|
||||
try {
|
||||
if (rawId) {
|
||||
const existing = await prisma.proclamation.findUnique({
|
||||
where: { id: rawId }
|
||||
});
|
||||
const existing = await prisma.proclamation.findUnique({ where: { id: rawId } });
|
||||
if (!existing) {
|
||||
return { success: false, error: 'Record not found' };
|
||||
return { success: false, action: 'upsert', error: 'Record not found' };
|
||||
}
|
||||
await prisma.proclamation.update({
|
||||
where: { id: rawId },
|
||||
data: data as any
|
||||
});
|
||||
await prisma.proclamation.update({ where: { id: rawId }, data: data as any });
|
||||
} else {
|
||||
await prisma.proclamation.create({ data: data as any });
|
||||
}
|
||||
@@ -80,18 +89,41 @@ export const actions = {
|
||||
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2002') {
|
||||
return {
|
||||
success: false,
|
||||
action: 'upsert',
|
||||
error: 'A record with this name already exists.'
|
||||
};
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
return { success: true, action: 'upsert' };
|
||||
},
|
||||
delete: async ({ request, locals }) => {
|
||||
if (!isSuper(locals)) {
|
||||
return { success: false, error: 'Only Super admins can delete proclamations' };
|
||||
return {
|
||||
success: false,
|
||||
action: 'delete',
|
||||
error: 'Only Super admins can delete proclamations'
|
||||
};
|
||||
}
|
||||
return deleteRecord(request, config, locals);
|
||||
|
||||
const formData = await request.formData();
|
||||
const rawId = formData.get('id')?.toString();
|
||||
if (!rawId) return { success: false, action: 'delete', error: 'Missing id' };
|
||||
|
||||
try {
|
||||
await prisma.proclamation.delete({ where: { id: rawId } });
|
||||
} catch (e: unknown) {
|
||||
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2003') {
|
||||
return {
|
||||
success: false,
|
||||
action: 'delete',
|
||||
error: 'Cannot delete: this proclamation is assigned to users.'
|
||||
};
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
return { success: true, action: 'delete' };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,95 +1,339 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { PageProps } from './$types';
|
||||
import type { ActionResult } from '$lib/svelte-admin/types';
|
||||
import AdminTable from '$lib/components/admin/AdminTable.svelte';
|
||||
import { Editor } from '@tiptap/core';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import Highlight from '@tiptap/extension-highlight';
|
||||
import { TextStyle } from '@tiptap/extension-text-style';
|
||||
import { Color } from '@tiptap/extension-color';
|
||||
import DataTable, { type DataTableColumn } from '$lib/components/DataTable.svelte';
|
||||
import AdminModal from '$lib/components/admin/AdminModal.svelte';
|
||||
import Toast from '$lib/components/admin/Toast.svelte';
|
||||
import { getModelConfig } from '$lib/svelte-admin/registry';
|
||||
import ProclamationForm from './ProclamationForm.svelte';
|
||||
|
||||
type ActionResult = {
|
||||
success?: boolean;
|
||||
action?: string;
|
||||
error?: string;
|
||||
fieldErrors?: Record<string, string>;
|
||||
} | null;
|
||||
|
||||
let { data, form }: PageProps = $props();
|
||||
|
||||
const config = getModelConfig('proclamations')!;
|
||||
const viewerRoleId = $derived(data.adminUser?.roleId ?? 4);
|
||||
const isSuper = $derived(viewerRoleId <= 1);
|
||||
const canDelete = $derived(viewerRoleId <= 2);
|
||||
|
||||
const columns: DataTableColumn[] = [
|
||||
{ label: 'Name', value: (r) => r.name },
|
||||
{
|
||||
label: 'Category',
|
||||
value: (r) => (r.category as Record<string, unknown> | undefined)?.name ?? ''
|
||||
},
|
||||
{ label: 'Group', value: (r) => (r.group as Record<string, unknown> | undefined)?.name ?? '' },
|
||||
{
|
||||
label: 'Frequency',
|
||||
value: (r) => (r.solfeggioFrequency as Record<string, unknown> | undefined)?.name ?? ''
|
||||
}
|
||||
];
|
||||
|
||||
let modalOpen = $state(false);
|
||||
let editingRecord = $state<Record<string, unknown> | null>(null);
|
||||
let toastMessage = $state('');
|
||||
let toastType = $state<'success' | 'error'>('success');
|
||||
let fieldErrors = $state<Record<string, string>>({});
|
||||
|
||||
let name = $state('');
|
||||
let solfeggioFrequencyId = $state('');
|
||||
let categoryId = $state('');
|
||||
let groupId = $state('');
|
||||
let content = $state('');
|
||||
|
||||
let formError = $state('');
|
||||
let fieldErrors = $state<Record<string, string>>({});
|
||||
let successMessage = $state('');
|
||||
let lastForm = $state<ActionResult>(null);
|
||||
|
||||
let element = $state<HTMLElement>();
|
||||
let editor: Editor | null = null;
|
||||
|
||||
let contentKey = $derived(editingRecord?.id ?? 'new');
|
||||
|
||||
$effect(() => {
|
||||
if (form && 'success' in (form as any)) {
|
||||
const result = form as any;
|
||||
if (result.success) {
|
||||
toastMessage = 'Saved successfully';
|
||||
toastType = 'success';
|
||||
closeModal();
|
||||
} else {
|
||||
formError = result.error ?? '';
|
||||
fieldErrors = result.fieldErrors ?? {};
|
||||
}
|
||||
const record = editingRecord ?? {};
|
||||
name = String(record.name ?? '');
|
||||
solfeggioFrequencyId = String(record.solfeggioFrequencyId ?? '');
|
||||
categoryId = String(record.categoryId ?? '');
|
||||
groupId = String(record.groupId ?? '');
|
||||
content = typeof record.content === 'string' ? record.content : '';
|
||||
if (!editingRecord) {
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
successMessage = '';
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!form || form === lastForm) return;
|
||||
lastForm = form;
|
||||
|
||||
if (form.success) {
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
successMessage = editingRecord ? 'Proclamation updated' : 'Proclamation created';
|
||||
} else {
|
||||
successMessage = '';
|
||||
formError = form.error ?? '';
|
||||
fieldErrors = form.fieldErrors ?? {};
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!isSuper || !modalOpen || !element) return;
|
||||
const initialContent =
|
||||
typeof (editingRecord ?? {})['content'] === 'string'
|
||||
? ((editingRecord ?? {})['content'] as string)
|
||||
: '';
|
||||
|
||||
editor = new Editor({
|
||||
element,
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
bulletList: false,
|
||||
orderedList: false
|
||||
}),
|
||||
TextStyle,
|
||||
Color,
|
||||
Highlight.configure({ multicolor: true })
|
||||
],
|
||||
content: initialContent,
|
||||
onUpdate: ({ editor }) => {
|
||||
content = editor.getHTML();
|
||||
}
|
||||
});
|
||||
|
||||
const instance = editor;
|
||||
instance.setEditable(true);
|
||||
|
||||
return () => {
|
||||
instance.destroy();
|
||||
};
|
||||
});
|
||||
|
||||
function toggleBold() {
|
||||
editor?.chain().focus().toggleBold().run();
|
||||
}
|
||||
function toggleItalic() {
|
||||
editor?.chain().focus().toggleItalic().run();
|
||||
}
|
||||
function toggleHighlight() {
|
||||
editor?.chain().focus().toggleHighlight().run();
|
||||
}
|
||||
|
||||
function setColor() {
|
||||
const current = editor?.getAttributes('textStyle').color ?? '#000000';
|
||||
const colors = ['#000000', '#FF0000', '#0000FF', '#008000', '#FFA500', '#800080', '#808080'];
|
||||
const idx = colors.indexOf(current);
|
||||
const next = colors[(idx + 1) % colors.length];
|
||||
editor?.chain().focus().setColor(next).run();
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editingRecord = null;
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
lastForm = null;
|
||||
modalOpen = true;
|
||||
}
|
||||
|
||||
function openEdit(record: Record<string, unknown>) {
|
||||
editingRecord = record;
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
lastForm = null;
|
||||
modalOpen = true;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modalOpen = false;
|
||||
editingRecord = null;
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<h1 class="mb-4 text-xl font-bold text-gray-900 dark:text-gray-100">{config.displayName}</h1>
|
||||
<AdminTable
|
||||
<div>
|
||||
<DataTable
|
||||
title="Proclamations"
|
||||
{columns}
|
||||
records={data.records}
|
||||
listFields={config.listFields}
|
||||
searchFields={config.searchFields}
|
||||
fieldTypes={config.fieldTypes}
|
||||
singular={config.displayNameSingular}
|
||||
newLabel="New Proclamation"
|
||||
searchPlaceholder="Search proclamations..."
|
||||
onRowClick={isSuper ? openEdit : undefined}
|
||||
onNew={isSuper ? openCreate : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AdminModal
|
||||
title={editingRecord
|
||||
? `Edit ${config.displayNameSingular}`
|
||||
: `Create ${config.displayNameSingular}`}
|
||||
open={modalOpen}
|
||||
onClose={closeModal}
|
||||
{canDelete}
|
||||
recordId={editingRecord?.id as string | undefined}
|
||||
>
|
||||
<ProclamationForm
|
||||
record={editingRecord ?? {}}
|
||||
relationOptionsData={data.relationOptionsData}
|
||||
categories={data.categories}
|
||||
{fieldErrors}
|
||||
{formError}
|
||||
{isSuper}
|
||||
submitLabel={editingRecord ? 'Update' : 'Create'}
|
||||
/>
|
||||
</AdminModal>
|
||||
{#if modalOpen}
|
||||
<AdminModal
|
||||
title={editingRecord ? 'Edit Proclamation' : 'Create Proclamation'}
|
||||
open={modalOpen}
|
||||
onClose={closeModal}
|
||||
canDelete={isSuper}
|
||||
recordId={editingRecord?.id as string | undefined}
|
||||
>
|
||||
{#if successMessage}
|
||||
<div
|
||||
class="mb-4 rounded-md border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700"
|
||||
>
|
||||
{successMessage}
|
||||
</div>
|
||||
{/if}
|
||||
{#if formError}
|
||||
<div class="mb-4 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{formError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if toastMessage}
|
||||
<Toast message={toastMessage} type={toastType} />
|
||||
<form method="POST" action="?/upsert" use:enhance class="flex flex-col gap-4">
|
||||
{#if editingRecord}
|
||||
<input type="hidden" name="id" value={String(editingRecord.id)} />
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<label for="field-name" class="mb-1 block text-sm font-medium text-slate-700">Name</label>
|
||||
<input
|
||||
id="field-name"
|
||||
type="text"
|
||||
name="name"
|
||||
required
|
||||
bind:value={name}
|
||||
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-indigo-500 focus:outline-none"
|
||||
/>
|
||||
{#if fieldErrors.name}
|
||||
<p class="mt-1 text-sm text-red-600">{fieldErrors.name}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="field-solfeggioFrequencyId"
|
||||
class="mb-1 block text-sm font-medium text-slate-700">Frequency</label
|
||||
>
|
||||
<select
|
||||
id="field-solfeggioFrequencyId"
|
||||
name="solfeggioFrequencyId"
|
||||
required
|
||||
bind:value={solfeggioFrequencyId}
|
||||
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-indigo-500 focus:outline-none"
|
||||
>
|
||||
<option value="">-- Select Frequency --</option>
|
||||
{#each data.frequencies as freq (String(freq.id))}
|
||||
<option value={String(freq.id)}>{freq.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if fieldErrors.solfeggioFrequencyId}
|
||||
<p class="mt-1 text-sm text-red-600">{fieldErrors.solfeggioFrequencyId}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="field-categoryId" class="mb-1 block text-sm font-medium text-slate-700"
|
||||
>Category</label
|
||||
>
|
||||
<select
|
||||
id="field-categoryId"
|
||||
name="categoryId"
|
||||
required
|
||||
bind:value={categoryId}
|
||||
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-indigo-500 focus:outline-none"
|
||||
>
|
||||
<option value="">-- Select Category --</option>
|
||||
{#each data.categories as cat (String(cat.id))}
|
||||
<option value={String(cat.id)}>{cat.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if fieldErrors.categoryId}
|
||||
<p class="mt-1 text-sm text-red-600">{fieldErrors.categoryId}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="field-groupId" class="mb-1 block text-sm font-medium text-slate-700"
|
||||
>Group</label
|
||||
>
|
||||
<select
|
||||
id="field-groupId"
|
||||
name="groupId"
|
||||
required
|
||||
bind:value={groupId}
|
||||
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-indigo-500 focus:outline-none"
|
||||
>
|
||||
<option value="">-- Select Group --</option>
|
||||
{#each data.groups as group (String(group.id))}
|
||||
<option value={String(group.id)}>{group.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if fieldErrors.groupId}
|
||||
<p class="mt-1 text-sm text-red-600">{fieldErrors.groupId}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="field-content" class="mb-1 block text-sm font-medium text-slate-700"
|
||||
>Content</label
|
||||
>
|
||||
<div class="rounded-md border border-slate-300 bg-white">
|
||||
<div class="flex gap-1 border-b border-slate-200 px-2 py-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onclick={toggleBold}
|
||||
title="Bold"
|
||||
class="rounded px-2 py-1 text-sm font-bold text-slate-600 transition hover:bg-slate-100"
|
||||
>B</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onclick={toggleItalic}
|
||||
title="Italic"
|
||||
class="rounded px-2 py-1 text-sm text-slate-600 italic transition hover:bg-slate-100"
|
||||
>I</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onclick={toggleHighlight}
|
||||
title="Highlight"
|
||||
class="rounded px-2 py-1 text-sm text-slate-600 transition hover:bg-slate-100"
|
||||
>H</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onclick={setColor}
|
||||
title="Cycle text color"
|
||||
class="rounded px-2 py-1 text-sm text-slate-600 transition hover:bg-slate-100"
|
||||
>A</button
|
||||
>
|
||||
</div>
|
||||
{#key contentKey}
|
||||
<div
|
||||
bind:this={element}
|
||||
data-testid="tiptap-editor"
|
||||
role="textbox"
|
||||
tabindex="0"
|
||||
contenteditable="true"
|
||||
class="px-3 py-2 text-sm text-slate-900 focus:outline-none"
|
||||
></div>
|
||||
{/key}
|
||||
<input type="hidden" name="content" value={content} />
|
||||
</div>
|
||||
{#if fieldErrors.content}
|
||||
<p class="mt-1 text-sm text-red-600">{fieldErrors.content}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onclick={closeModal}
|
||||
class="rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-indigo-700"
|
||||
>
|
||||
{editingRecord ? 'Save Proclamation' : 'Create Proclamation'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AdminModal>
|
||||
{/if}
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import TipTap from '$lib/components/tiptap/TipTap.svelte';
|
||||
|
||||
let {
|
||||
record = {} as Record<string, unknown>,
|
||||
fieldErrors = {} as Record<string, string>,
|
||||
formError = '',
|
||||
relationOptionsData = {} as Record<string, { label: string; value: string | number }[]>,
|
||||
categories = [] as { id: number; name: string; parentId: number | null }[],
|
||||
isSuper = false,
|
||||
action = '?/upsert',
|
||||
submitLabel = 'Save'
|
||||
}: {
|
||||
record?: Record<string, unknown>;
|
||||
fieldErrors?: Record<string, string>;
|
||||
formError?: string;
|
||||
relationOptionsData?: Record<string, { label: string; value: string | number }[]>;
|
||||
categories?: { id: number; name: string; parentId: number | null }[];
|
||||
isSuper?: boolean;
|
||||
action?: string;
|
||||
submitLabel?: string;
|
||||
} = $props();
|
||||
|
||||
let dirty = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
dirty = false;
|
||||
});
|
||||
|
||||
function markDirty() {
|
||||
if (record.id) dirty = true;
|
||||
}
|
||||
|
||||
let name = $state('');
|
||||
let solfeggioFrequencyId = $state('');
|
||||
let subscriptionLevelId = $state('');
|
||||
let parentCategoryId = $state('');
|
||||
let categoryId = $state('');
|
||||
|
||||
$effect(() => {
|
||||
name = String(record.name ?? '');
|
||||
solfeggioFrequencyId = String(record.solfeggioFrequencyId ?? '');
|
||||
subscriptionLevelId = String(record.subscriptionLevelId ?? '');
|
||||
parentCategoryId = String(
|
||||
record.parentCategoryId ??
|
||||
(record.category as Record<string, unknown> | undefined)?.parentId ??
|
||||
''
|
||||
);
|
||||
categoryId = String(record.categoryId ?? '');
|
||||
});
|
||||
|
||||
let parentOptions = $derived(categories.filter((c) => c.parentId === null));
|
||||
|
||||
let childOptions = $derived(
|
||||
parentCategoryId ? categories.filter((c) => c.parentId === Number(parentCategoryId)) : []
|
||||
);
|
||||
|
||||
function handleParentChange(e: Event) {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
parentCategoryId = target.value;
|
||||
categoryId = '';
|
||||
}
|
||||
|
||||
let contentKey = $derived(record.id ?? 'new');
|
||||
</script>
|
||||
|
||||
<form {action} method="POST" use:enhance oninput={markDirty}>
|
||||
{#if record?.id}
|
||||
<input type="hidden" name="id" value={String(record.id)} />
|
||||
{/if}
|
||||
|
||||
{#if formError}
|
||||
<div class="mb-4 border border-red-300 bg-red-50 px-4 py-2 text-sm text-red-700">
|
||||
{formError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<label
|
||||
for="field-name"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="field-name"
|
||||
name="name"
|
||||
bind:value={name}
|
||||
class="w-full border px-3 py-1.5 text-sm {fieldErrors.name
|
||||
? 'border-red-400'
|
||||
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
{#if fieldErrors.name}
|
||||
<p class="mt-1 text-xs text-red-600">{fieldErrors.name}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="field-solfeggioFrequencyId"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Frequency
|
||||
</label>
|
||||
<select
|
||||
id="field-solfeggioFrequencyId"
|
||||
name="solfeggioFrequencyId"
|
||||
onchange={(e) => (solfeggioFrequencyId = (e.target as HTMLSelectElement).value)}
|
||||
class="w-full border px-3 py-1.5 text-sm {fieldErrors.solfeggioFrequencyId
|
||||
? 'border-red-400'
|
||||
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
|
||||
>
|
||||
<option value="">-- Select Frequency --</option>
|
||||
{#each relationOptionsData.solfeggioFrequencyId ?? [] as opt (String(opt.value))}
|
||||
<option value={opt.value} selected={String(opt.value) === solfeggioFrequencyId}
|
||||
>{opt.label}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
{#if fieldErrors.solfeggioFrequencyId}
|
||||
<p class="mt-1 text-xs text-red-600">{fieldErrors.solfeggioFrequencyId}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="field-parentCategoryId"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Category
|
||||
</label>
|
||||
<select
|
||||
id="field-parentCategoryId"
|
||||
name="parentCategoryId"
|
||||
onchange={handleParentChange}
|
||||
class="w-full border px-3 py-1.5 text-sm {fieldErrors.parentCategoryId
|
||||
? 'border-red-400'
|
||||
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
|
||||
>
|
||||
<option value="">-- Select Category --</option>
|
||||
{#each parentOptions as cat (String(cat.id))}
|
||||
<option value={cat.id} selected={String(cat.id) === parentCategoryId}>{cat.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if fieldErrors.parentCategoryId}
|
||||
<p class="mt-1 text-xs text-red-600">{fieldErrors.parentCategoryId}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="field-categoryId"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Sub-Category
|
||||
</label>
|
||||
<select
|
||||
id="field-categoryId"
|
||||
name="categoryId"
|
||||
onchange={(e) => (categoryId = (e.target as HTMLSelectElement).value)}
|
||||
class="w-full border px-3 py-1.5 text-sm {fieldErrors.categoryId
|
||||
? 'border-red-400'
|
||||
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
|
||||
>
|
||||
<option value="">-- Select Sub-Category --</option>
|
||||
{#each childOptions as cat (String(cat.id))}
|
||||
<option value={cat.id} selected={String(cat.id) === categoryId}>{cat.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if fieldErrors.categoryId}
|
||||
<p class="mt-1 text-xs text-red-600">{fieldErrors.categoryId}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="field-subscriptionLevelId"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Subscription Level
|
||||
</label>
|
||||
<select
|
||||
id="field-subscriptionLevelId"
|
||||
name="subscriptionLevelId"
|
||||
onchange={(e) => (subscriptionLevelId = (e.target as HTMLSelectElement).value)}
|
||||
class="w-full border px-3 py-1.5 text-sm {fieldErrors.subscriptionLevelId
|
||||
? 'border-red-400'
|
||||
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
|
||||
>
|
||||
<option value="">-- Select Level --</option>
|
||||
{#each relationOptionsData.subscriptionLevelId ?? [] as opt (String(opt.value))}
|
||||
<option value={opt.value} selected={String(opt.value) === subscriptionLevelId}
|
||||
>{opt.label}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
{#if fieldErrors.subscriptionLevelId}
|
||||
<p class="mt-1 text-xs text-red-600">{fieldErrors.subscriptionLevelId}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if isSuper}
|
||||
<div>
|
||||
<label
|
||||
for="field-content"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Content
|
||||
</label>
|
||||
<div id="field-content">
|
||||
{#key record.id}
|
||||
<TipTap
|
||||
initialContent={typeof record.content === 'string' ? record.content : ''}
|
||||
name="content"
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
{#if fieldErrors.content}
|
||||
<p class="mt-1 text-xs text-red-600">{fieldErrors.content}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!!record?.id && !dirty}
|
||||
class="border border-gray-300 bg-gray-100 px-4 py-1.5 text-sm font-medium hover:bg-gray-200 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
>
|
||||
{submitLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,152 +0,0 @@
|
||||
import { page } from 'vitest/browser';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render } from 'vitest-browser-svelte';
|
||||
|
||||
vi.mock('$app/forms', () => ({
|
||||
enhance: () => () => {}
|
||||
}));
|
||||
|
||||
vi.mock('$lib/components/tiptap/TipTap.svelte', () => ({
|
||||
default: ($anchor: Element) => {
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('data-testid', 'tiptap-mock');
|
||||
$anchor.replaceWith(div);
|
||||
}
|
||||
}));
|
||||
|
||||
import ProclamationForm from './ProclamationForm.svelte';
|
||||
|
||||
const relationOptionsData = {
|
||||
solfeggioFrequencyId: [
|
||||
{ label: 'UT - 396', value: 1 },
|
||||
{ label: 'RE - 417', value: 2 }
|
||||
],
|
||||
subscriptionLevelId: [
|
||||
{ label: 'Free', value: 1 },
|
||||
{ label: 'Premium', value: 2 }
|
||||
]
|
||||
};
|
||||
|
||||
const categories = [
|
||||
{ id: 1, name: 'Healing', parentId: null },
|
||||
{ id: 2, name: 'Manifestation', parentId: null },
|
||||
{ id: 3, name: 'Physical Healing', parentId: 1 },
|
||||
{ id: 4, name: 'Emotional Healing', parentId: 1 }
|
||||
];
|
||||
|
||||
describe('ProclamationForm', () => {
|
||||
it('renders all basic fields in create mode', async () => {
|
||||
render(ProclamationForm, {
|
||||
relationOptionsData,
|
||||
categories,
|
||||
isSuper: true
|
||||
});
|
||||
|
||||
await expect.element(page.getByRole('textbox', { name: 'Name' })).toBeInTheDocument();
|
||||
await expect.element(page.getByRole('combobox', { name: /^Frequency$/ })).toBeInTheDocument();
|
||||
await expect.element(page.getByRole('combobox', { name: /^Category$/ })).toBeInTheDocument();
|
||||
await expect
|
||||
.element(page.getByRole('combobox', { name: /^Sub-Category$/ }))
|
||||
.toBeInTheDocument();
|
||||
await expect
|
||||
.element(page.getByRole('combobox', { name: /^Subscription Level$/ }))
|
||||
.toBeInTheDocument();
|
||||
await expect.element(page.getByRole('button', { name: 'Save' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('pre-fills name input when editing an existing record', async () => {
|
||||
render(ProclamationForm, {
|
||||
record: {
|
||||
id: 'proc-1',
|
||||
name: 'Test Proclamation',
|
||||
solfeggioFrequencyId: 2,
|
||||
subscriptionLevelId: 2,
|
||||
categoryId: 3,
|
||||
parentCategoryId: 1
|
||||
},
|
||||
relationOptionsData,
|
||||
categories,
|
||||
isSuper: true,
|
||||
submitLabel: 'Update'
|
||||
});
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('textbox', { name: 'Name' }))
|
||||
.toHaveValue('Test Proclamation');
|
||||
});
|
||||
|
||||
it('pre-fills select values when editing an existing record', async () => {
|
||||
render(ProclamationForm, {
|
||||
record: {
|
||||
id: 'proc-1',
|
||||
name: 'Test Proclamation',
|
||||
solfeggioFrequencyId: 2,
|
||||
subscriptionLevelId: 2,
|
||||
categoryId: 3,
|
||||
parentCategoryId: 1
|
||||
},
|
||||
relationOptionsData,
|
||||
categories,
|
||||
isSuper: true
|
||||
});
|
||||
|
||||
const freqEl = (await page
|
||||
.getByRole('combobox', { name: /^Frequency$/ })
|
||||
.element()) as HTMLSelectElement;
|
||||
const levelEl = (await page
|
||||
.getByRole('combobox', { name: /^Subscription Level$/ })
|
||||
.element()) as HTMLSelectElement;
|
||||
|
||||
expect(freqEl.value).toBe('2');
|
||||
expect(levelEl.value).toBe('2');
|
||||
});
|
||||
|
||||
it('shows content field when isSuper is true', async () => {
|
||||
render(ProclamationForm, {
|
||||
record: { id: 'proc-1', name: 'Test', content: '<p>Secret</p>' },
|
||||
relationOptionsData,
|
||||
categories,
|
||||
isSuper: true
|
||||
});
|
||||
|
||||
await expect.element(page.getByText('Content')).toBeInTheDocument();
|
||||
await expect.element(page.getByTestId('tiptap-mock')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides content field when isSuper is false', async () => {
|
||||
render(ProclamationForm, {
|
||||
record: { id: 'proc-1', name: 'Test', content: '<p>Secret</p>' },
|
||||
relationOptionsData,
|
||||
categories,
|
||||
isSuper: false
|
||||
});
|
||||
|
||||
await expect.element(page.getByText('Content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables submit button when editing and not dirty', async () => {
|
||||
render(ProclamationForm, {
|
||||
record: { id: 'proc-1', name: 'Test' },
|
||||
relationOptionsData,
|
||||
categories,
|
||||
isSuper: true,
|
||||
submitLabel: 'Update'
|
||||
});
|
||||
|
||||
await expect.element(page.getByRole('button', { name: 'Update' })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('enables submit button when editing and user modifies a field', async () => {
|
||||
render(ProclamationForm, {
|
||||
record: { id: 'proc-1', name: 'Test' },
|
||||
relationOptionsData,
|
||||
categories,
|
||||
isSuper: true,
|
||||
submitLabel: 'Update'
|
||||
});
|
||||
|
||||
await page.getByRole('textbox', { name: 'Name' }).fill('New Name');
|
||||
|
||||
await expect.element(page.getByRole('button', { name: 'Update' })).toBeEnabled();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { isHttpError } from '@sveltejs/kit';
|
||||
import { createLocals, mockUser, createFormRequest } from '$lib/test-utils';
|
||||
import { createLocals, mockUser, createFormRequest, mockProclamation } from '$lib/test-utils';
|
||||
|
||||
vi.mock('$lib/db', () => ({
|
||||
prisma: {
|
||||
@@ -11,29 +10,44 @@ vi.mock('$lib/db', () => ({
|
||||
update: vi.fn(),
|
||||
delete: vi.fn()
|
||||
},
|
||||
solfeggioFrequency: {
|
||||
findMany: vi.fn()
|
||||
},
|
||||
proclamationCategory: {
|
||||
findMany: vi.fn()
|
||||
},
|
||||
proclamationGroup: {
|
||||
findMany: vi.fn()
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('$lib/svelte-admin/crud', () => ({
|
||||
loadRecords: vi.fn(),
|
||||
deleteRecord: vi.fn(),
|
||||
superFormErrorsToFieldErrors: vi.fn(() => ({ error: '', fieldErrors: {} }))
|
||||
}));
|
||||
|
||||
import { prisma } from '$lib/db';
|
||||
import { loadRecords, deleteRecord, superFormErrorsToFieldErrors } from '$lib/svelte-admin/crud';
|
||||
import { load, actions } from './+page.server';
|
||||
|
||||
const mockLoadRecords = loadRecords as ReturnType<typeof vi.fn>;
|
||||
const mockDeleteRecord = deleteRecord as ReturnType<typeof vi.fn>;
|
||||
const mockSuperFormErrorsToFieldErrors = superFormErrorsToFieldErrors as ReturnType<typeof vi.fn>;
|
||||
const mockProclamationFindMany = prisma.proclamation.findMany as ReturnType<typeof vi.fn>;
|
||||
const mockProclamationFindUnique = prisma.proclamation.findUnique as ReturnType<typeof vi.fn>;
|
||||
const mockProclamationCreate = prisma.proclamation.create as ReturnType<typeof vi.fn>;
|
||||
const mockProclamationUpdate = prisma.proclamation.update as ReturnType<typeof vi.fn>;
|
||||
const mockProclamationDelete = prisma.proclamation.delete as ReturnType<typeof vi.fn>;
|
||||
const mockFreqFindMany = prisma.solfeggioFrequency.findMany as ReturnType<typeof vi.fn>;
|
||||
const mockCatFindMany = prisma.proclamationCategory.findMany as ReturnType<typeof vi.fn>;
|
||||
const mockFindUnique = prisma.proclamation.findUnique as ReturnType<typeof vi.fn>;
|
||||
const mockCreate = prisma.proclamation.create as ReturnType<typeof vi.fn>;
|
||||
const mockUpdate = prisma.proclamation.update as ReturnType<typeof vi.fn>;
|
||||
const mockGroupFindMany = prisma.proclamationGroup.findMany as ReturnType<typeof vi.fn>;
|
||||
|
||||
const frequencies = [{ id: 1, name: 'UT', value: 396 }];
|
||||
const categories = [{ id: 1, name: 'Healing' }];
|
||||
const groups = [{ id: 1, name: 'General' }];
|
||||
|
||||
function validRequest(overrides: Record<string, string> = {}) {
|
||||
return createFormRequest({
|
||||
name: 'Test Proclamation',
|
||||
solfeggioFrequencyId: '1',
|
||||
categoryId: '1',
|
||||
groupId: '1',
|
||||
content: '<p>Hello</p>',
|
||||
...overrides
|
||||
});
|
||||
}
|
||||
|
||||
describe('proclamations admin page server', () => {
|
||||
beforeEach(() => {
|
||||
@@ -45,37 +59,42 @@ describe('proclamations admin page server', () => {
|
||||
{ id: '1', name: 'A', content: '<p>secret</p>' },
|
||||
{ id: '2', name: 'B', content: '<p>secret2</p>' }
|
||||
];
|
||||
const relationOptionsData = {};
|
||||
const categories = [{ id: 1, name: 'Healing', parentId: null }];
|
||||
|
||||
beforeEach(() => {
|
||||
mockProclamationFindMany.mockResolvedValue(recordsWithContent);
|
||||
mockFreqFindMany.mockResolvedValue(frequencies);
|
||||
mockCatFindMany.mockResolvedValue(categories);
|
||||
mockGroupFindMany.mockResolvedValue(groups);
|
||||
});
|
||||
|
||||
it('strips content for non-Super users', async () => {
|
||||
mockLoadRecords.mockResolvedValue({ records: recordsWithContent, relationOptionsData });
|
||||
mockCatFindMany.mockResolvedValue(categories);
|
||||
|
||||
const locals = createLocals(mockUser({ roleId: 4 }) as any);
|
||||
const result = await load({ locals } as any);
|
||||
|
||||
expect(mockProclamationFindMany).toHaveBeenCalledWith({
|
||||
include: { category: true, group: true, solfeggioFrequency: true },
|
||||
orderBy: { name: 'asc' }
|
||||
});
|
||||
expect(result).toEqual({
|
||||
records: [
|
||||
{ id: '1', name: 'A' },
|
||||
{ id: '2', name: 'B' }
|
||||
],
|
||||
relationOptionsData,
|
||||
categories
|
||||
frequencies,
|
||||
categories,
|
||||
groups
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps content for Super users', async () => {
|
||||
mockLoadRecords.mockResolvedValue({ records: recordsWithContent, relationOptionsData });
|
||||
mockCatFindMany.mockResolvedValue(categories);
|
||||
|
||||
const locals = createLocals(mockUser({ roleId: 1 }) as any);
|
||||
const result = await load({ locals } as any);
|
||||
|
||||
expect(result).toEqual({
|
||||
records: recordsWithContent,
|
||||
relationOptionsData,
|
||||
categories
|
||||
frequencies,
|
||||
categories,
|
||||
groups
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -86,7 +105,7 @@ describe('proclamations admin page server', () => {
|
||||
const req = createFormRequest({ name: 'test' });
|
||||
|
||||
const result = await actions.upsert({ request: req, locals } as any);
|
||||
expect(result).toEqual({ success: false, error: 'Unauthorized' });
|
||||
expect(result).toEqual({ success: false, action: 'upsert', error: 'Unauthorized' });
|
||||
});
|
||||
|
||||
it('rejects non-Super users', async () => {
|
||||
@@ -96,40 +115,133 @@ describe('proclamations admin page server', () => {
|
||||
const result = await actions.upsert({ request: req, locals } as any);
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
action: 'upsert',
|
||||
error: 'Only Super admins can edit proclamations'
|
||||
});
|
||||
});
|
||||
|
||||
it('returns field errors for invalid input', async () => {
|
||||
const locals = createLocals(mockUser({ roleId: 1 }) as any);
|
||||
const req = validRequest({ solfeggioFrequencyId: 'not-a-number' });
|
||||
|
||||
const result = await actions.upsert({ request: req, locals } as any);
|
||||
expect(result.success).toBe(false);
|
||||
expect(
|
||||
(result as { fieldErrors: Record<string, string> }).fieldErrors.solfeggioFrequencyId
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('creates a proclamation for a Super user', async () => {
|
||||
mockProclamationCreate.mockResolvedValue({ id: 'p1' });
|
||||
|
||||
const locals = createLocals(mockUser({ roleId: 1 }) as any);
|
||||
const result = await actions.upsert({ request: validRequest(), locals } as any);
|
||||
|
||||
expect(result).toEqual({ success: true, action: 'upsert' });
|
||||
expect(mockProclamationCreate).toHaveBeenCalledWith({
|
||||
data: {
|
||||
name: 'Test Proclamation',
|
||||
solfeggioFrequencyId: 1,
|
||||
categoryId: 1,
|
||||
groupId: 1,
|
||||
content: '<p>Hello</p>'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('updates an existing proclamation for a Super user', async () => {
|
||||
mockProclamationFindUnique.mockResolvedValue(mockProclamation());
|
||||
mockProclamationUpdate.mockResolvedValue({});
|
||||
|
||||
const locals = createLocals(mockUser({ roleId: 1 }) as any);
|
||||
const result = await actions.upsert({
|
||||
request: validRequest({ id: 'proc-1', name: 'Renamed' }),
|
||||
locals
|
||||
} as any);
|
||||
|
||||
expect(result).toEqual({ success: true, action: 'upsert' });
|
||||
expect(mockProclamationUpdate).toHaveBeenCalledWith({
|
||||
where: { id: 'proc-1' },
|
||||
data: {
|
||||
name: 'Renamed',
|
||||
solfeggioFrequencyId: 1,
|
||||
categoryId: 1,
|
||||
groupId: 1,
|
||||
content: '<p>Hello</p>'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('returns record-not-found for a missing id', async () => {
|
||||
mockProclamationFindUnique.mockResolvedValue(null);
|
||||
|
||||
const locals = createLocals(mockUser({ roleId: 1 }) as any);
|
||||
const result = await actions.upsert({
|
||||
request: validRequest({ id: 'missing' }),
|
||||
locals
|
||||
} as any);
|
||||
|
||||
expect(result).toEqual({ success: false, action: 'upsert', error: 'Record not found' });
|
||||
});
|
||||
|
||||
it('returns a friendly error on duplicate name (P2002)', async () => {
|
||||
mockProclamationCreate.mockRejectedValue({ code: 'P2002' });
|
||||
|
||||
const locals = createLocals(mockUser({ roleId: 1 }) as any);
|
||||
const result = await actions.upsert({ request: validRequest(), locals } as any);
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
action: 'upsert',
|
||||
error: 'A record with this name already exists.'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete action', () => {
|
||||
it('rejects non-Super users', async () => {
|
||||
const locals = createLocals(mockUser({ roleId: 4 }) as any);
|
||||
const req = new Request('http://localhost', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ id: '1' })
|
||||
});
|
||||
const req = createFormRequest({ id: '1' });
|
||||
|
||||
const result = await actions.delete({ request: req, locals } as any);
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
action: 'delete',
|
||||
error: 'Only Super admins can delete proclamations'
|
||||
});
|
||||
});
|
||||
|
||||
it('delegates to deleteRecord for Super users', async () => {
|
||||
mockDeleteRecord.mockResolvedValue({ success: true });
|
||||
|
||||
it('returns an error when id is missing', async () => {
|
||||
const locals = createLocals(mockUser({ roleId: 1 }) as any);
|
||||
const req = new Request('http://localhost', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ id: '1' })
|
||||
});
|
||||
const req = createFormRequest({});
|
||||
|
||||
const result = await actions.delete({ request: req, locals } as any);
|
||||
expect(mockDeleteRecord).toHaveBeenCalled();
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(result).toEqual({ success: false, action: 'delete', error: 'Missing id' });
|
||||
});
|
||||
|
||||
it('deletes a proclamation for a Super user', async () => {
|
||||
mockProclamationDelete.mockResolvedValue({});
|
||||
|
||||
const locals = createLocals(mockUser({ roleId: 1 }) as any);
|
||||
const req = createFormRequest({ id: 'proc-1' });
|
||||
|
||||
const result = await actions.delete({ request: req, locals } as any);
|
||||
expect(result).toEqual({ success: true, action: 'delete' });
|
||||
expect(mockProclamationDelete).toHaveBeenCalledWith({ where: { id: 'proc-1' } });
|
||||
});
|
||||
|
||||
it('returns a friendly error when the proclamation is assigned to users (P2003)', async () => {
|
||||
mockProclamationDelete.mockRejectedValue({ code: 'P2003' });
|
||||
|
||||
const locals = createLocals(mockUser({ roleId: 1 }) as any);
|
||||
const req = createFormRequest({ id: 'proc-1' });
|
||||
|
||||
const result = await actions.delete({ request: req, locals } as any);
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
action: 'delete',
|
||||
error: 'Cannot delete: this proclamation is assigned to users.'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import { page } from 'vitest/browser';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render } from 'vitest-browser-svelte';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
vi.mock('$app/forms', () => ({
|
||||
enhance: () => () => {}
|
||||
}));
|
||||
|
||||
import Page from './+page.svelte';
|
||||
|
||||
const frequencies = [
|
||||
{ id: 1, name: 'UT', value: 396 },
|
||||
{ id: 2, name: 'RE', value: 417 }
|
||||
];
|
||||
|
||||
const categories = [
|
||||
{ id: 1, name: 'Healing' },
|
||||
{ id: 2, name: 'Manifestation' }
|
||||
];
|
||||
|
||||
const groups = [
|
||||
{ id: 1, name: 'General' },
|
||||
{ id: 2, name: 'Specific' }
|
||||
];
|
||||
|
||||
const superAdmin = {
|
||||
id: 'admin-1',
|
||||
name: 'Super Admin',
|
||||
roleId: 1,
|
||||
roleName: 'SUPER'
|
||||
};
|
||||
|
||||
function makeData(overrides: Partial<PageProps['data']> = {}): PageProps['data'] {
|
||||
return {
|
||||
records: [],
|
||||
frequencies,
|
||||
categories,
|
||||
groups,
|
||||
adminUser: superAdmin,
|
||||
userRoleId: 1,
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
function mountPage(dataOverrides: Partial<PageProps['data']> = {}) {
|
||||
return render(Page, { params: {}, data: makeData(dataOverrides), form: null });
|
||||
}
|
||||
|
||||
const editRecord = {
|
||||
id: 'proc-1',
|
||||
name: 'Test Proclamation',
|
||||
content: '<p>Secret</p>',
|
||||
solfeggioFrequencyId: 2,
|
||||
categoryId: 1,
|
||||
groupId: 2,
|
||||
category: { name: 'Healing' },
|
||||
group: { name: 'Specific' },
|
||||
solfeggioFrequency: { name: 'RE' }
|
||||
};
|
||||
|
||||
async function clickButton(name: string) {
|
||||
const loc = page.getByRole('button', { name });
|
||||
const el = (await loc.element()) as HTMLElement;
|
||||
el.click();
|
||||
}
|
||||
|
||||
async function clickCell(name: string) {
|
||||
const loc = page.getByRole('cell', { name });
|
||||
const el = (await loc.element()) as HTMLElement;
|
||||
el.click();
|
||||
}
|
||||
|
||||
async function openCreateModal() {
|
||||
await clickButton('New Proclamation');
|
||||
}
|
||||
|
||||
async function openEditModal() {
|
||||
await clickCell('Test Proclamation');
|
||||
}
|
||||
|
||||
describe('proclamations admin page (super user)', () => {
|
||||
it('renders all basic fields in create mode', async () => {
|
||||
mountPage();
|
||||
|
||||
await openCreateModal();
|
||||
|
||||
await expect.element(page.getByRole('textbox', { name: 'Name' })).toBeInTheDocument();
|
||||
await expect.element(page.getByRole('combobox', { name: /^Frequency$/ })).toBeInTheDocument();
|
||||
await expect.element(page.getByRole('combobox', { name: /^Category$/ })).toBeInTheDocument();
|
||||
await expect.element(page.getByRole('combobox', { name: /^Group$/ })).toBeInTheDocument();
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'Create Proclamation' }))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('pre-fills name input when editing an existing record', async () => {
|
||||
mountPage({ records: [editRecord] });
|
||||
|
||||
await openEditModal();
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('textbox', { name: 'Name' }))
|
||||
.toHaveValue('Test Proclamation');
|
||||
});
|
||||
|
||||
it('pre-fills select values when editing an existing record', async () => {
|
||||
mountPage({ records: [editRecord] });
|
||||
|
||||
await openEditModal();
|
||||
|
||||
const freqEl = (await page
|
||||
.getByRole('combobox', { name: /^Frequency$/ })
|
||||
.element()) as HTMLSelectElement;
|
||||
const categoryEl = (await page
|
||||
.getByRole('combobox', { name: /^Category$/ })
|
||||
.element()) as HTMLSelectElement;
|
||||
const groupEl = (await page
|
||||
.getByRole('combobox', { name: /^Group$/ })
|
||||
.element()) as HTMLSelectElement;
|
||||
|
||||
expect(freqEl.value).toBe('2');
|
||||
expect(categoryEl.value).toBe('1');
|
||||
expect(groupEl.value).toBe('2');
|
||||
});
|
||||
|
||||
it('keeps group selection independent of category selection (movable filter)', async () => {
|
||||
mountPage({ records: [editRecord] });
|
||||
|
||||
await openEditModal();
|
||||
|
||||
const categoryEl = (await page
|
||||
.getByRole('combobox', { name: /^Category$/ })
|
||||
.element()) as HTMLSelectElement;
|
||||
const groupEl = (await page
|
||||
.getByRole('combobox', { name: /^Group$/ })
|
||||
.element()) as HTMLSelectElement;
|
||||
|
||||
categoryEl.value = '2';
|
||||
categoryEl.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
|
||||
expect(categoryEl.value).toBe('2');
|
||||
expect(groupEl.value).toBe('2');
|
||||
});
|
||||
|
||||
it('shows content editor when isSuper is true', async () => {
|
||||
mountPage({ records: [editRecord] });
|
||||
|
||||
await openEditModal();
|
||||
|
||||
await expect.element(page.getByText('Content')).toBeInTheDocument();
|
||||
await expect.element(page.getByTestId('tiptap-editor')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('proclamations admin page (non-super user)', () => {
|
||||
it('hides create/edit entry points and content field from non-super users', async () => {
|
||||
mountPage({
|
||||
records: [editRecord],
|
||||
adminUser: { ...superAdmin, id: 'admin-2', roleId: 2, roleName: 'ADMIN' }
|
||||
});
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'New Proclamation' }))
|
||||
.not.toBeInTheDocument();
|
||||
await expect.element(page.getByText('Content')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,49 +1,146 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getModelConfig } from '$lib/svelte-admin/registry';
|
||||
import {
|
||||
loadRecords,
|
||||
upsertRecord,
|
||||
deleteRecord,
|
||||
superFormErrorsToFieldErrors
|
||||
} from '$lib/svelte-admin/crud';
|
||||
import { prisma } from '$lib/db';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { superValidate } from 'sveltekit-superforms/server';
|
||||
import { zod4 } from 'sveltekit-superforms/adapters';
|
||||
import * as Schemas from '$lib/svelte-admin/schemas';
|
||||
import { z } from 'zod';
|
||||
|
||||
const config = getModelConfig('profiles')!;
|
||||
import { superFormErrorsToFieldErrors } from '$lib/superform-errors';
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
return loadRecords(locals, config);
|
||||
const email = z.string().email('Invalid email address');
|
||||
const phone = z
|
||||
.string()
|
||||
.min(10, 'Phone number must be at least 10 digits')
|
||||
.regex(/^[\d\-\+\(\) ]+$/, 'Invalid phone number format');
|
||||
const name = z.string().min(1, 'Required').max(100, 'Too long');
|
||||
|
||||
const ProfileSchema = z.object({
|
||||
email,
|
||||
firstName: name,
|
||||
lastName: name,
|
||||
phoneNumber: phone
|
||||
});
|
||||
|
||||
const AddressSchema = z.object({
|
||||
profileId: z.string().min(1),
|
||||
street: z.string().min(1, 'Street is required'),
|
||||
city: z.string().min(1, 'City is required'),
|
||||
state: z.string().min(1, 'State is required'),
|
||||
postalCode: z.string().min(1, 'Postal code is required'),
|
||||
country: z.string().min(1, 'Country is required'),
|
||||
type: z.enum(['Mailing', 'Billing', 'Shipping']).default('Mailing'),
|
||||
isDefaultMailing: z.coerce.boolean().default(false),
|
||||
isDefaultBilling: z.coerce.boolean().default(false),
|
||||
isDefaultShipping: z.coerce.boolean().default(false)
|
||||
});
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
const records = await prisma.profile.findMany({
|
||||
include: { user: true, addresses: true },
|
||||
orderBy: { id: 'asc' }
|
||||
});
|
||||
|
||||
return {
|
||||
records: records as unknown as Record<string, unknown>[]
|
||||
};
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
upsert: async ({ request, locals }) => {
|
||||
return upsertRecord(request, config, locals);
|
||||
},
|
||||
delete: async ({ request, locals }) => {
|
||||
return deleteRecord(request, config, locals);
|
||||
},
|
||||
upsertAddress: async ({ request, locals }) => {
|
||||
if (!locals.user) throw error(401, 'Unauthorized');
|
||||
|
||||
const form = await superValidate(request, zod4(Schemas.Address));
|
||||
upsert: async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
const form = await superValidate(formData, zod4(ProfileSchema));
|
||||
if (!form.valid) {
|
||||
const { error: addrError, fieldErrors } = superFormErrorsToFieldErrors(
|
||||
const { error: formError, fieldErrors } = superFormErrorsToFieldErrors(
|
||||
form.errors as Record<string, unknown>
|
||||
);
|
||||
return { success: false, addressError: addrError, addressFieldErrors: fieldErrors };
|
||||
return { success: false, action: 'upsert', error: formError, fieldErrors };
|
||||
}
|
||||
|
||||
const rawId = formData.get('id')?.toString();
|
||||
const data = {
|
||||
email: form.data.email,
|
||||
firstName: form.data.firstName,
|
||||
lastName: form.data.lastName,
|
||||
phoneNumber: form.data.phoneNumber
|
||||
};
|
||||
|
||||
if (rawId) {
|
||||
const existing = await prisma.profile.findUnique({
|
||||
where: { id: rawId },
|
||||
include: { user: true }
|
||||
});
|
||||
if (!existing) {
|
||||
return { success: false, action: 'upsert', error: 'Record not found' };
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = { ...data };
|
||||
|
||||
if (existing.userId) {
|
||||
if (existing.user?.email !== data.email) {
|
||||
const conflict = await prisma.user.findFirst({
|
||||
where: { email: data.email, id: { not: existing.userId } }
|
||||
});
|
||||
if (conflict) {
|
||||
return {
|
||||
success: false,
|
||||
action: 'upsert',
|
||||
error: 'That email already belongs to another user.'
|
||||
};
|
||||
}
|
||||
await prisma.user.update({ where: { id: existing.userId }, data: { email: data.email } });
|
||||
}
|
||||
} else {
|
||||
const match = await prisma.user.findUnique({ where: { email: data.email } });
|
||||
if (match) updateData.userId = match.id;
|
||||
}
|
||||
|
||||
await prisma.profile.update({ where: { id: rawId }, data: updateData });
|
||||
return { success: true, action: 'upsert' };
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await prisma.user.findUnique({ where: { email: data.email } });
|
||||
await prisma.profile.create({ data: { ...data, ...(user ? { userId: user.id } : {}) } });
|
||||
} catch (e: unknown) {
|
||||
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2002') {
|
||||
return {
|
||||
success: false,
|
||||
action: 'upsert',
|
||||
error: 'A profile for this email already exists.'
|
||||
};
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
return { success: true, action: 'upsert' };
|
||||
},
|
||||
delete: async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
const rawId = formData.get('id')?.toString();
|
||||
if (!rawId) return { success: false, error: 'Missing id' };
|
||||
await prisma.profile.delete({ where: { id: rawId } });
|
||||
return { success: true, action: 'delete' };
|
||||
},
|
||||
upsertAddress: async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
const form = await superValidate(formData, zod4(AddressSchema));
|
||||
if (!form.valid) {
|
||||
const { error: addressError, fieldErrors } = superFormErrorsToFieldErrors(
|
||||
form.errors as Record<string, unknown>
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
action: 'upsertAddress',
|
||||
addressError,
|
||||
addressFieldErrors: fieldErrors
|
||||
};
|
||||
}
|
||||
|
||||
const { profileId, isDefaultMailing, isDefaultBilling, isDefaultShipping, ...addressData } =
|
||||
form.data;
|
||||
|
||||
if (!profileId) {
|
||||
return { success: false, addressError: 'Missing profile ID' };
|
||||
return { success: false, action: 'upsertAddress', addressError: 'Missing profile ID' };
|
||||
}
|
||||
|
||||
// Unique-default enforcement: clear defaults of same type before setting
|
||||
if (isDefaultMailing) {
|
||||
await prisma.address.updateMany({
|
||||
where: { profileId, isDefaultMailing: true },
|
||||
@@ -72,32 +169,6 @@ export const actions = {
|
||||
};
|
||||
|
||||
const created = await prisma.address.create({ data: payload });
|
||||
return { success: true, address: created };
|
||||
},
|
||||
updateUser: async ({ request, locals }) => {
|
||||
if (!locals.user) throw error(401, 'Unauthorized');
|
||||
|
||||
const form = await superValidate(request, zod4(Schemas.UserUpdate));
|
||||
if (!form.valid) {
|
||||
const { error: userErr, fieldErrors } = superFormErrorsToFieldErrors(
|
||||
form.errors as Record<string, unknown>
|
||||
);
|
||||
return { success: false, userError: userErr, userFieldErrors: fieldErrors };
|
||||
}
|
||||
|
||||
const { id, email, name } = form.data;
|
||||
if (!id) {
|
||||
return { success: false, userError: 'Missing user ID' };
|
||||
}
|
||||
|
||||
const data: Record<string, string> = {};
|
||||
if (email) data.email = email;
|
||||
if (name) data.name = name;
|
||||
if (Object.keys(data).length === 0) {
|
||||
return { success: false, userError: 'No fields to update' };
|
||||
}
|
||||
|
||||
await prisma.user.update({ where: { id }, data });
|
||||
return { success: true };
|
||||
return { success: true, action: 'upsertAddress', address: created };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,487 +1,59 @@
|
||||
<script lang="ts">
|
||||
import type { PageProps } from './$types';
|
||||
import type { ActionResult } from '$lib/svelte-admin/types';
|
||||
import AdminTable from '$lib/components/admin/AdminTable.svelte';
|
||||
import AdminModal from '$lib/components/admin/AdminModal.svelte';
|
||||
import Toast from '$lib/components/admin/Toast.svelte';
|
||||
import { enhance } from '$app/forms';
|
||||
import { getModelConfig } from '$lib/svelte-admin/registry';
|
||||
import DataTable, { type DataTableColumn } from '$lib/components/DataTable.svelte';
|
||||
import CreateProfileModal from './CreateProfileModal.svelte';
|
||||
import EditProfileModal from './EditProfileModal.svelte';
|
||||
|
||||
let { data, form }: PageProps = $props();
|
||||
|
||||
const config = getModelConfig('profiles')!;
|
||||
let canDelete = $derived((data.adminUser?.roleId ?? 4) <= 2);
|
||||
|
||||
let modalOpen = $state(false);
|
||||
let editingRecord = $state<Record<string, unknown> | null>(null);
|
||||
let toastMessage = $state('');
|
||||
let toastType = $state<'success' | 'error'>('success');
|
||||
let fieldErrors = $state<Record<string, string>>({});
|
||||
let formError = $state('');
|
||||
let addressFormOpen = $state(false);
|
||||
let userFormOpen = $state(false);
|
||||
let addressError = $state('');
|
||||
let userError = $state('');
|
||||
const columns: DataTableColumn[] = [
|
||||
{ label: 'First Name', value: (r) => r.firstName },
|
||||
{ label: 'Last Name', value: (r) => r.lastName },
|
||||
{
|
||||
label: 'Email',
|
||||
value: (r) => (r.user as Record<string, unknown> | undefined)?.email ?? r.email ?? ''
|
||||
},
|
||||
{ label: 'Phone', value: (r) => r.phoneNumber },
|
||||
{ label: 'User', value: (r) => (r.user as Record<string, unknown> | undefined)?.name ?? '' }
|
||||
];
|
||||
|
||||
let addressFields = $state<Record<string, string>>({
|
||||
street: '',
|
||||
city: '',
|
||||
state: '',
|
||||
postalCode: '',
|
||||
country: '',
|
||||
isDefaultMailing: 'false',
|
||||
isDefaultBilling: 'false',
|
||||
isDefaultShipping: 'false'
|
||||
});
|
||||
|
||||
let userEmail = $state('');
|
||||
let userName = $state('');
|
||||
|
||||
$effect(() => {
|
||||
if (form) {
|
||||
if ((form as any).addressError) {
|
||||
addressError = (form as any).addressError;
|
||||
} else if ((form as any).userError) {
|
||||
userError = (form as any).userError;
|
||||
} else if (form.success) {
|
||||
toastType = 'success';
|
||||
if ((form as any).address) {
|
||||
toastMessage = 'Address saved';
|
||||
const addr = (form as any).address as Record<string, unknown>;
|
||||
if (editingRecord) {
|
||||
const addrs = (editingRecord.addresses as Record<string, unknown>[]) ?? [];
|
||||
editingRecord = {
|
||||
...editingRecord,
|
||||
addresses: [...addrs, addr]
|
||||
};
|
||||
}
|
||||
addressFormOpen = false;
|
||||
addressError = '';
|
||||
} else {
|
||||
toastMessage = 'Saved successfully';
|
||||
closeModal();
|
||||
}
|
||||
} else if (!(form as any).addressError && !(form as any).userError) {
|
||||
formError = (form as ActionResult).error ?? '';
|
||||
fieldErrors = (form as ActionResult).fieldErrors ?? {};
|
||||
}
|
||||
}
|
||||
});
|
||||
let createOpen = $state(false);
|
||||
let editRecord = $state<Record<string, unknown> | null>(null);
|
||||
|
||||
function openCreate() {
|
||||
editingRecord = null;
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
addressFormOpen = false;
|
||||
userFormOpen = false;
|
||||
modalOpen = true;
|
||||
createOpen = true;
|
||||
}
|
||||
|
||||
function openEdit(record: Record<string, unknown>) {
|
||||
editingRecord = record;
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
addressFormOpen = false;
|
||||
userFormOpen = false;
|
||||
addressError = '';
|
||||
userError = '';
|
||||
userEmail = ((record.user as Record<string, unknown>)?.email as string) ?? '';
|
||||
userName = ((record.user as Record<string, unknown>)?.name as string) ?? '';
|
||||
modalOpen = true;
|
||||
editRecord = record;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modalOpen = false;
|
||||
editingRecord = null;
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
addressFormOpen = false;
|
||||
userFormOpen = false;
|
||||
addressError = '';
|
||||
userError = '';
|
||||
function closeCreate() {
|
||||
createOpen = false;
|
||||
}
|
||||
|
||||
function toggleAddressForm() {
|
||||
addressFormOpen = !addressFormOpen;
|
||||
addressError = '';
|
||||
}
|
||||
|
||||
function toggleUserForm() {
|
||||
userFormOpen = !userFormOpen;
|
||||
userError = '';
|
||||
function closeEdit() {
|
||||
editRecord = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<h1 class="mb-4 text-xl font-bold text-gray-900 dark:text-gray-100">{config.displayName}</h1>
|
||||
<AdminTable
|
||||
<div>
|
||||
<DataTable
|
||||
title="User Profiles"
|
||||
{columns}
|
||||
records={data.records}
|
||||
listFields={config.listFields}
|
||||
searchFields={config.searchFields}
|
||||
fieldTypes={config.fieldTypes}
|
||||
singular={config.displayNameSingular}
|
||||
newLabel="New Profile"
|
||||
searchPlaceholder="Search profiles..."
|
||||
onRowClick={openEdit}
|
||||
onNew={openCreate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AdminModal
|
||||
title={editingRecord
|
||||
? `Edit ${config.displayNameSingular}`
|
||||
: `Create ${config.displayNameSingular}`}
|
||||
open={modalOpen}
|
||||
onClose={closeModal}
|
||||
{canDelete}
|
||||
recordId={editingRecord?.id as string | undefined}
|
||||
>
|
||||
<form method="POST" action="?/upsert" use:enhance>
|
||||
{#if editingRecord?.id}
|
||||
<input type="hidden" name="id" value={String(editingRecord.id)} />
|
||||
{/if}
|
||||
{#if formError}
|
||||
<div class="mb-4 border border-red-300 bg-red-50 px-4 py-2 text-sm text-red-700">
|
||||
{formError}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<label
|
||||
for="field-userId"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
User
|
||||
</label>
|
||||
<select
|
||||
id="field-userId"
|
||||
name="userId"
|
||||
value={String(editingRecord?.userId ?? '')}
|
||||
class="w-full border px-3 py-1.5 text-sm {fieldErrors.userId
|
||||
? 'border-red-400'
|
||||
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
|
||||
>
|
||||
<option value="">-- Select User --</option>
|
||||
{#each data.relationOptionsData.userId ?? [] as opt (String(opt.value))}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if fieldErrors.userId}
|
||||
<p class="mt-1 text-xs text-red-600">{fieldErrors.userId}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="field-firstName"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
First Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="field-firstName"
|
||||
name="firstName"
|
||||
value={String(editingRecord?.firstName ?? '')}
|
||||
class="w-full border px-3 py-1.5 text-sm {fieldErrors.firstName
|
||||
? 'border-red-400'
|
||||
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
{#if fieldErrors.firstName}
|
||||
<p class="mt-1 text-xs text-red-600">{fieldErrors.firstName}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="field-lastName"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Last Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="field-lastName"
|
||||
name="lastName"
|
||||
value={String(editingRecord?.lastName ?? '')}
|
||||
class="w-full border px-3 py-1.5 text-sm {fieldErrors.lastName
|
||||
? 'border-red-400'
|
||||
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
{#if fieldErrors.lastName}
|
||||
<p class="mt-1 text-xs text-red-600">{fieldErrors.lastName}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="field-phoneNumber"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Phone Number
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="field-phoneNumber"
|
||||
name="phoneNumber"
|
||||
value={String(editingRecord?.phoneNumber ?? '')}
|
||||
class="w-full border px-3 py-1.5 text-sm {fieldErrors.phoneNumber
|
||||
? 'border-red-400'
|
||||
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
{#if fieldErrors.phoneNumber}
|
||||
<p class="mt-1 text-xs text-red-600">{fieldErrors.phoneNumber}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
class="border border-gray-300 bg-gray-100 px-4 py-1.5 text-sm font-medium hover:bg-gray-200 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
>
|
||||
{editingRecord ? 'Update' : 'Create'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<CreateProfileModal open={createOpen} onClose={closeCreate} {form} />
|
||||
|
||||
{#if editingRecord}
|
||||
{@const user = editingRecord.user as Record<string, unknown> | undefined}
|
||||
<div class="mt-6 border-t border-gray-200 pt-4 dark:border-gray-700">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">User</h3>
|
||||
<button
|
||||
type="button"
|
||||
onclick={toggleUserForm}
|
||||
class="text-xs font-medium text-blue-600 hover:text-blue-800 dark:text-blue-400"
|
||||
>
|
||||
{userFormOpen ? 'Cancel' : 'Edit User'}
|
||||
</button>
|
||||
</div>
|
||||
<p class="mb-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
{(user?.name as string) ?? ''} ({(user?.email as string) ?? ''})
|
||||
</p>
|
||||
|
||||
{#if userFormOpen}
|
||||
<form
|
||||
method="POST"
|
||||
action="?/updateUser"
|
||||
use:enhance
|
||||
class="mt-3 rounded border border-gray-200 p-3 dark:border-gray-700"
|
||||
>
|
||||
<input type="hidden" name="id" value={String(user?.id ?? '')} />
|
||||
{#if userError}
|
||||
<div class="mb-2 border border-red-300 bg-red-50 px-3 py-1.5 text-sm text-red-700">
|
||||
{userError}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-col gap-2">
|
||||
<div>
|
||||
<label
|
||||
for="user-name"
|
||||
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="user-name"
|
||||
name="name"
|
||||
value={userName}
|
||||
class="w-full border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="user-email"
|
||||
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="user-email"
|
||||
name="email"
|
||||
value={userEmail}
|
||||
class="w-full border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
class="border border-gray-300 bg-gray-100 px-3 py-1 text-xs font-medium hover:bg-gray-200 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
>
|
||||
Save User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if editingRecord}
|
||||
{@const addresses = (editingRecord.addresses as Record<string, unknown>[]) ?? []}
|
||||
<div class="mt-6 border-t border-gray-200 pt-4 dark:border-gray-700">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Addresses</h3>
|
||||
<button
|
||||
type="button"
|
||||
onclick={toggleAddressForm}
|
||||
class="text-xs font-medium text-blue-600 hover:text-blue-800 dark:text-blue-400"
|
||||
>
|
||||
{addressFormOpen ? 'Cancel' : 'Add Address +'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if addressFormOpen}
|
||||
<form
|
||||
method="POST"
|
||||
action="?/upsertAddress"
|
||||
use:enhance
|
||||
class="mb-3 rounded border border-gray-200 p-3 dark:border-gray-700"
|
||||
>
|
||||
<input type="hidden" name="profileId" value={String(editingRecord.id)} />
|
||||
{#if addressError}
|
||||
<div class="mb-2 border border-red-300 bg-red-50 px-3 py-1.5 text-sm text-red-700">
|
||||
{addressError}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label
|
||||
for="addr-street"
|
||||
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
Street
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="addr-street"
|
||||
name="street"
|
||||
required
|
||||
class="w-full border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="addr-city"
|
||||
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
City
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="addr-city"
|
||||
name="city"
|
||||
required
|
||||
class="w-full border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="addr-state"
|
||||
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
State
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="addr-state"
|
||||
name="state"
|
||||
required
|
||||
class="w-full border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="addr-postal"
|
||||
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
Postal Code
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="addr-postal"
|
||||
name="postalCode"
|
||||
required
|
||||
class="w-full border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="addr-country"
|
||||
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
Country
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="addr-country"
|
||||
name="country"
|
||||
required
|
||||
class="w-full border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 flex flex-wrap gap-4">
|
||||
<label class="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-400">
|
||||
<input type="checkbox" name="isDefaultMailing" class="h-3.5 w-3.5" />
|
||||
Default Mailing
|
||||
</label>
|
||||
<label class="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-400">
|
||||
<input type="checkbox" name="isDefaultBilling" class="h-3.5 w-3.5" />
|
||||
Default Billing
|
||||
</label>
|
||||
<label class="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-400">
|
||||
<input type="checkbox" name="isDefaultShipping" class="h-3.5 w-3.5" />
|
||||
Default Shipping
|
||||
</label>
|
||||
</div>
|
||||
<div class="mt-2 flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
class="border border-gray-300 bg-gray-100 px-3 py-1 text-xs font-medium hover:bg-gray-200 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
>
|
||||
Save Address
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
{#if addresses.length > 0}
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-200 dark:border-gray-700">
|
||||
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase">Street</th
|
||||
>
|
||||
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase">City</th>
|
||||
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase">State</th>
|
||||
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase">Zip</th>
|
||||
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase"
|
||||
>Country</th
|
||||
>
|
||||
<th class="px-2 py-1 text-center text-xs font-medium text-gray-500 uppercase">M</th>
|
||||
<th class="px-2 py-1 text-center text-xs font-medium text-gray-500 uppercase">B</th>
|
||||
<th class="px-2 py-1 text-center text-xs font-medium text-gray-500 uppercase">S</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each addresses as addr}
|
||||
<tr class="border-b border-gray-100 dark:border-gray-800">
|
||||
<td class="px-2 py-1">{addr.street as string}</td>
|
||||
<td class="px-2 py-1">{addr.city as string}</td>
|
||||
<td class="px-2 py-1">{addr.state as string}</td>
|
||||
<td class="px-2 py-1">{addr.postalCode as string}</td>
|
||||
<td class="px-2 py-1">{addr.country as string}</td>
|
||||
<td class="px-2 py-1 text-center text-xs">{addr.isDefaultMailing ? '✓' : ''}</td>
|
||||
<td class="px-2 py-1 text-center text-xs">{addr.isDefaultBilling ? '✓' : ''}</td>
|
||||
<td class="px-2 py-1 text-center text-xs">{addr.isDefaultShipping ? '✓' : ''}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{:else}
|
||||
<p class="text-sm text-gray-500">No addresses on file.</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</AdminModal>
|
||||
|
||||
{#if toastMessage}
|
||||
<Toast message={toastMessage} type={toastType} />
|
||||
{#if editRecord}
|
||||
{#key editRecord.id}
|
||||
<EditProfileModal record={editRecord} open={true} onClose={closeEdit} {canDelete} {form} />
|
||||
{/key}
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import AdminModal from '$lib/components/admin/AdminModal.svelte';
|
||||
import ProfileFields from './ProfileFields.svelte';
|
||||
|
||||
type ActionResult = {
|
||||
success?: boolean;
|
||||
action?: string;
|
||||
error?: string;
|
||||
fieldErrors?: Record<string, string>;
|
||||
} | null;
|
||||
|
||||
let {
|
||||
open = false,
|
||||
onClose,
|
||||
form
|
||||
}: {
|
||||
open?: boolean;
|
||||
onClose?: () => void;
|
||||
form?: ActionResult;
|
||||
} = $props();
|
||||
|
||||
let formError = $state('');
|
||||
let fieldErrors = $state<Record<string, string>>({});
|
||||
let successMessage = $state('');
|
||||
let lastForm = $state<ActionResult>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
successMessage = '';
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!form || form === lastForm) return;
|
||||
lastForm = form;
|
||||
|
||||
if (form.success) {
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
successMessage = 'Profile created';
|
||||
setTimeout(() => onClose?.(), 1200);
|
||||
} else {
|
||||
successMessage = '';
|
||||
formError = form.error ?? '';
|
||||
fieldErrors = form.fieldErrors ?? {};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<AdminModal title="Create Profile" {open} {onClose}>
|
||||
{#if successMessage}
|
||||
<div
|
||||
class="mb-4 rounded-md border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700"
|
||||
>
|
||||
{successMessage}
|
||||
</div>
|
||||
{/if}
|
||||
{#if formError}
|
||||
<div class="mb-4 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{formError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form method="POST" action="?/upsert" use:enhance>
|
||||
<ProfileFields record={null} {fieldErrors} />
|
||||
|
||||
<div class="mt-6 flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onclick={onClose}
|
||||
class="rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-indigo-700"
|
||||
>
|
||||
Create Profile
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AdminModal>
|
||||
@@ -0,0 +1,305 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import AdminModal from '$lib/components/admin/AdminModal.svelte';
|
||||
import ProfileFields from './ProfileFields.svelte';
|
||||
|
||||
type ActionResult = {
|
||||
success?: boolean;
|
||||
action?: string;
|
||||
error?: string;
|
||||
fieldErrors?: Record<string, string>;
|
||||
addressError?: string;
|
||||
address?: Record<string, unknown>;
|
||||
} | null;
|
||||
|
||||
let {
|
||||
record,
|
||||
open = false,
|
||||
onClose,
|
||||
canDelete = false,
|
||||
form
|
||||
}: {
|
||||
record: Record<string, unknown>;
|
||||
open?: boolean;
|
||||
onClose?: () => void;
|
||||
canDelete?: boolean;
|
||||
form?: ActionResult;
|
||||
} = $props();
|
||||
|
||||
let formError = $state('');
|
||||
let fieldErrors = $state<Record<string, string>>({});
|
||||
let addressError = $state('');
|
||||
let successMessage = $state('');
|
||||
let addressFormOpen = $state(false);
|
||||
let addresses = $state<Record<string, unknown>[]>([]);
|
||||
|
||||
let addrStreet = $state('');
|
||||
let addrCity = $state('');
|
||||
let addrState = $state('');
|
||||
let addrPostal = $state('');
|
||||
let addrCountry = $state('');
|
||||
let addrMailing = $state(false);
|
||||
let addrBilling = $state(false);
|
||||
let addrShipping = $state(false);
|
||||
|
||||
let lastRecordId = $state<string | null>(null);
|
||||
let lastForm = $state<ActionResult>(null);
|
||||
|
||||
function resetAddressFields() {
|
||||
addrStreet = '';
|
||||
addrCity = '';
|
||||
addrState = '';
|
||||
addrPostal = '';
|
||||
addrCountry = '';
|
||||
addrMailing = false;
|
||||
addrBilling = false;
|
||||
addrShipping = false;
|
||||
}
|
||||
|
||||
function seedRecord() {
|
||||
addresses = (record.addresses as Record<string, unknown>[]) ?? [];
|
||||
addressFormOpen = false;
|
||||
addressError = '';
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
successMessage = '';
|
||||
resetAddressFields();
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
seedRecord();
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const id = String(record.id ?? '');
|
||||
if (id === lastRecordId) return;
|
||||
lastRecordId = id;
|
||||
seedRecord();
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!form || form === lastForm) return;
|
||||
lastForm = form;
|
||||
|
||||
switch (form.action) {
|
||||
case 'upsertAddress':
|
||||
if (form.success && form.address) {
|
||||
addresses = [...addresses, form.address];
|
||||
addressFormOpen = false;
|
||||
addressError = '';
|
||||
successMessage = 'Address saved';
|
||||
resetAddressFields();
|
||||
} else {
|
||||
successMessage = '';
|
||||
addressError = form.addressError ?? '';
|
||||
}
|
||||
break;
|
||||
case 'upsert':
|
||||
default:
|
||||
if (form.success) {
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
successMessage = 'Profile updated';
|
||||
} else {
|
||||
successMessage = '';
|
||||
formError = form.error ?? '';
|
||||
fieldErrors = form.fieldErrors ?? {};
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
function toggleAddressForm() {
|
||||
addressFormOpen = !addressFormOpen;
|
||||
addressError = '';
|
||||
if (addressFormOpen) resetAddressFields();
|
||||
}
|
||||
</script>
|
||||
|
||||
<AdminModal title="Edit Profile" {open} {onClose} {canDelete} recordId={String(record.id ?? '')}>
|
||||
{#if successMessage}
|
||||
<div
|
||||
class="mb-4 rounded-md border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700"
|
||||
>
|
||||
{successMessage}
|
||||
</div>
|
||||
{/if}
|
||||
{#if formError}
|
||||
<div class="mb-4 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{formError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form method="POST" action="?/upsert" use:enhance>
|
||||
<input type="hidden" name="id" value={String(record.id ?? '')} />
|
||||
|
||||
<ProfileFields {record} {fieldErrors} />
|
||||
|
||||
<div class="mt-6 flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onclick={onClose}
|
||||
class="rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-indigo-700"
|
||||
>
|
||||
Save Profile
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-8 border-t border-slate-200 pt-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold text-slate-900">Addresses</h3>
|
||||
<button
|
||||
type="button"
|
||||
onclick={toggleAddressForm}
|
||||
class="rounded-md border border-slate-300 bg-white px-3 py-1.5 text-xs font-medium text-slate-700 transition hover:bg-slate-50"
|
||||
>
|
||||
{addressFormOpen ? 'Cancel' : 'Add Address +'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if addressFormOpen}
|
||||
<form method="POST" action="?/upsertAddress" use:enhance class="mt-4 flex flex-col gap-4">
|
||||
<input type="hidden" name="profileId" value={String(record.id ?? '')} />
|
||||
{#if addressError}
|
||||
<div class="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{addressError}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="addr-street" class="mb-1 block text-sm font-medium text-slate-700"
|
||||
>Street</label
|
||||
>
|
||||
<input
|
||||
id="addr-street"
|
||||
type="text"
|
||||
name="street"
|
||||
required
|
||||
bind:value={addrStreet}
|
||||
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-indigo-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="addr-city" class="mb-1 block text-sm font-medium text-slate-700">City</label
|
||||
>
|
||||
<input
|
||||
id="addr-city"
|
||||
type="text"
|
||||
name="city"
|
||||
required
|
||||
bind:value={addrCity}
|
||||
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-indigo-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="addr-state" class="mb-1 block text-sm font-medium text-slate-700"
|
||||
>State</label
|
||||
>
|
||||
<input
|
||||
id="addr-state"
|
||||
type="text"
|
||||
name="state"
|
||||
required
|
||||
bind:value={addrState}
|
||||
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-indigo-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="addr-postal" class="mb-1 block text-sm font-medium text-slate-700"
|
||||
>Postal Code</label
|
||||
>
|
||||
<input
|
||||
id="addr-postal"
|
||||
type="text"
|
||||
name="postalCode"
|
||||
required
|
||||
bind:value={addrPostal}
|
||||
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-indigo-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="addr-country" class="mb-1 block text-sm font-medium text-slate-700"
|
||||
>Country</label
|
||||
>
|
||||
<input
|
||||
id="addr-country"
|
||||
type="text"
|
||||
name="country"
|
||||
required
|
||||
bind:value={addrCountry}
|
||||
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-indigo-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-4 text-sm text-slate-700">
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="checkbox" name="isDefaultMailing" bind:checked={addrMailing} />
|
||||
Default Mailing
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="checkbox" name="isDefaultBilling" bind:checked={addrBilling} />
|
||||
Default Billing
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="checkbox" name="isDefaultShipping" bind:checked={addrShipping} />
|
||||
Default Shipping
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-slate-800 px-4 py-2 text-sm font-medium text-white transition hover:bg-slate-700"
|
||||
>
|
||||
Save Address
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
{#if addresses.length > 0}
|
||||
<div class="mt-4 overflow-x-auto rounded-md border border-slate-200">
|
||||
<table class="w-full min-w-max text-left text-sm text-slate-700">
|
||||
<thead>
|
||||
<tr
|
||||
class="border-b border-slate-200 bg-slate-100 text-xs font-semibold tracking-wide text-slate-500"
|
||||
>
|
||||
<th scope="col" class="px-3 py-2">Street</th>
|
||||
<th scope="col" class="px-3 py-2">City</th>
|
||||
<th scope="col" class="px-3 py-2">State</th>
|
||||
<th scope="col" class="px-3 py-2">Zip</th>
|
||||
<th scope="col" class="px-3 py-2">Country</th>
|
||||
<th scope="col" class="px-3 py-2">M</th>
|
||||
<th scope="col" class="px-3 py-2">B</th>
|
||||
<th scope="col" class="px-3 py-2">S</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each addresses as addr, i (String(addr.id ?? i))}
|
||||
<tr class="bg-white">
|
||||
<td class="px-3 py-2 whitespace-nowrap">{addr.street as string}</td>
|
||||
<td class="px-3 py-2 whitespace-nowrap">{addr.city as string}</td>
|
||||
<td class="px-3 py-2 whitespace-nowrap">{addr.state as string}</td>
|
||||
<td class="px-3 py-2 whitespace-nowrap">{addr.postalCode as string}</td>
|
||||
<td class="px-3 py-2 whitespace-nowrap">{addr.country as string}</td>
|
||||
<td class="px-3 py-2 whitespace-nowrap">{addr.isDefaultMailing ? '✓' : ''}</td>
|
||||
<td class="px-3 py-2 whitespace-nowrap">{addr.isDefaultBilling ? '✓' : ''}</td>
|
||||
<td class="px-3 py-2 whitespace-nowrap">{addr.isDefaultShipping ? '✓' : ''}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="mt-2 text-sm text-slate-600">No addresses on file.</p>
|
||||
{/if}
|
||||
</div>
|
||||
</AdminModal>
|
||||
@@ -0,0 +1,92 @@
|
||||
<script lang="ts">
|
||||
let {
|
||||
record = null,
|
||||
fieldErrors = {} as Record<string, string>
|
||||
}: {
|
||||
record?: Record<string, unknown> | null;
|
||||
fieldErrors?: Record<string, string>;
|
||||
} = $props();
|
||||
|
||||
let email = $state('');
|
||||
let firstName = $state('');
|
||||
let lastName = $state('');
|
||||
let phoneNumber = $state('');
|
||||
|
||||
$effect(() => {
|
||||
const rec = record ?? {};
|
||||
const user = rec.user as Record<string, unknown> | undefined;
|
||||
email = String((user?.email as string) ?? rec.email ?? '');
|
||||
firstName = String(rec.firstName ?? '');
|
||||
lastName = String(rec.lastName ?? '');
|
||||
phoneNumber = String(rec.phoneNumber ?? '');
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<label for="field-email" class="mb-1 block text-sm font-medium text-slate-700">Email</label>
|
||||
<input
|
||||
id="field-email"
|
||||
type="email"
|
||||
name="email"
|
||||
required
|
||||
bind:value={email}
|
||||
placeholder="user@example.com"
|
||||
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-indigo-500 focus:outline-none"
|
||||
/>
|
||||
{#if fieldErrors.email}
|
||||
<p class="mt-1 text-sm text-red-600">{fieldErrors.email}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="field-firstName" class="mb-1 block text-sm font-medium text-slate-700"
|
||||
>First Name</label
|
||||
>
|
||||
<input
|
||||
id="field-firstName"
|
||||
type="text"
|
||||
name="firstName"
|
||||
required
|
||||
bind:value={firstName}
|
||||
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-indigo-500 focus:outline-none"
|
||||
/>
|
||||
{#if fieldErrors.firstName}
|
||||
<p class="mt-1 text-sm text-red-600">{fieldErrors.firstName}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="field-lastName" class="mb-1 block text-sm font-medium text-slate-700"
|
||||
>Last Name</label
|
||||
>
|
||||
<input
|
||||
id="field-lastName"
|
||||
type="text"
|
||||
name="lastName"
|
||||
required
|
||||
bind:value={lastName}
|
||||
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-indigo-500 focus:outline-none"
|
||||
/>
|
||||
{#if fieldErrors.lastName}
|
||||
<p class="mt-1 text-sm text-red-600">{fieldErrors.lastName}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="field-phoneNumber" class="mb-1 block text-sm font-medium text-slate-700">
|
||||
Phone Number
|
||||
</label>
|
||||
<input
|
||||
id="field-phoneNumber"
|
||||
type="text"
|
||||
name="phoneNumber"
|
||||
required
|
||||
bind:value={phoneNumber}
|
||||
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-indigo-500 focus:outline-none"
|
||||
/>
|
||||
{#if fieldErrors.phoneNumber}
|
||||
<p class="mt-1 text-sm text-red-600">{fieldErrors.phoneNumber}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,233 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createLocals, createFormRequest } from '$lib/test-utils';
|
||||
|
||||
vi.mock('$lib/db', () => ({
|
||||
prisma: {
|
||||
profile: {
|
||||
findMany: vi.fn(),
|
||||
findUnique: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn()
|
||||
},
|
||||
user: {
|
||||
findUnique: vi.fn(),
|
||||
findFirst: vi.fn(),
|
||||
update: vi.fn()
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
import { prisma } from '$lib/db';
|
||||
import { load, actions } from './+page.server';
|
||||
|
||||
const mockProfileFindMany = prisma.profile.findMany as ReturnType<typeof vi.fn>;
|
||||
const mockProfileFindUnique = prisma.profile.findUnique as ReturnType<typeof vi.fn>;
|
||||
const mockProfileCreate = prisma.profile.create as ReturnType<typeof vi.fn>;
|
||||
const mockProfileUpdate = prisma.profile.update as ReturnType<typeof vi.fn>;
|
||||
const mockProfileDelete = prisma.profile.delete as ReturnType<typeof vi.fn>;
|
||||
const mockUserFindUnique = prisma.user.findUnique as ReturnType<typeof vi.fn>;
|
||||
const mockUserFindFirst = prisma.user.findFirst as ReturnType<typeof vi.fn>;
|
||||
const mockUserUpdate = prisma.user.update as ReturnType<typeof vi.fn>;
|
||||
|
||||
const validFields = {
|
||||
email: 'jane@example.com',
|
||||
firstName: 'Jane',
|
||||
lastName: 'Doe',
|
||||
phoneNumber: '555-010-1234'
|
||||
};
|
||||
|
||||
function makeProfileRequest(overrides: Record<string, string> = {}) {
|
||||
return createFormRequest({ ...validFields, ...overrides });
|
||||
}
|
||||
|
||||
describe('profiles admin page server', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('load', () => {
|
||||
it('returns profiles with user and addresses', async () => {
|
||||
const records = [
|
||||
{
|
||||
id: 'p1',
|
||||
email: null,
|
||||
user: { email: 'jane@example.com', name: 'Jane Doe' },
|
||||
addresses: []
|
||||
}
|
||||
];
|
||||
mockProfileFindMany.mockResolvedValue(records);
|
||||
|
||||
const result = await load({} as any);
|
||||
|
||||
expect(mockProfileFindMany).toHaveBeenCalledWith({
|
||||
include: { user: true, addresses: true },
|
||||
orderBy: { id: 'asc' }
|
||||
});
|
||||
expect(result).toEqual({ records });
|
||||
});
|
||||
});
|
||||
|
||||
describe('upsert - create', () => {
|
||||
it('links the profile to an existing user with the same email', async () => {
|
||||
mockUserFindUnique.mockResolvedValue({ id: 'user-1', email: validFields.email });
|
||||
mockProfileCreate.mockResolvedValue({ id: 'p-new' });
|
||||
|
||||
const result = await actions.upsert({ request: makeProfileRequest() } as any);
|
||||
|
||||
expect(result).toEqual({ success: true, action: 'upsert' });
|
||||
expect(mockUserFindUnique).toHaveBeenCalledWith({ where: { email: validFields.email } });
|
||||
expect(mockProfileCreate).toHaveBeenCalledWith({
|
||||
data: { ...validFields, userId: 'user-1' }
|
||||
});
|
||||
});
|
||||
|
||||
it('stores the email on the profile when no user matches', async () => {
|
||||
mockUserFindUnique.mockResolvedValue(null);
|
||||
mockProfileCreate.mockResolvedValue({ id: 'p-new' });
|
||||
|
||||
const result = await actions.upsert({ request: makeProfileRequest() } as any);
|
||||
|
||||
expect(result).toEqual({ success: true, action: 'upsert' });
|
||||
expect(mockProfileCreate).toHaveBeenCalledWith({ data: validFields });
|
||||
});
|
||||
|
||||
it('returns a friendly error when the email already exists (P2002)', async () => {
|
||||
mockUserFindUnique.mockResolvedValue(null);
|
||||
mockProfileCreate.mockRejectedValue({ code: 'P2002' });
|
||||
|
||||
const result = await actions.upsert({ request: makeProfileRequest() } as any);
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
action: 'upsert',
|
||||
error: 'A profile for this email already exists.'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('upsert - update', () => {
|
||||
it('syncs the linked user email when it changes', async () => {
|
||||
mockProfileFindUnique.mockResolvedValue({
|
||||
id: 'p1',
|
||||
userId: 'user-1',
|
||||
email: 'jane@example.com',
|
||||
user: { email: 'old@example.com' }
|
||||
});
|
||||
mockUserFindFirst.mockResolvedValue(null);
|
||||
mockUserUpdate.mockResolvedValue({});
|
||||
mockProfileUpdate.mockResolvedValue({});
|
||||
|
||||
const request = makeProfileRequest({ email: 'new@example.com', id: 'p1' });
|
||||
const result = await actions.upsert({ request } as any);
|
||||
|
||||
expect(result).toEqual({ success: true, action: 'upsert' });
|
||||
expect(mockUserUpdate).toHaveBeenCalledWith({
|
||||
where: { id: 'user-1' },
|
||||
data: { email: 'new@example.com' }
|
||||
});
|
||||
expect(mockProfileUpdate).toHaveBeenCalledWith({
|
||||
where: { id: 'p1' },
|
||||
data: { ...validFields, email: 'new@example.com' }
|
||||
});
|
||||
});
|
||||
|
||||
it('does not touch the user when the email is unchanged', async () => {
|
||||
mockProfileFindUnique.mockResolvedValue({
|
||||
id: 'p1',
|
||||
userId: 'user-1',
|
||||
email: 'jane@example.com',
|
||||
user: { email: 'jane@example.com' }
|
||||
});
|
||||
mockProfileUpdate.mockResolvedValue({});
|
||||
|
||||
const request = makeProfileRequest({ id: 'p1' });
|
||||
const result = await actions.upsert({ request } as any);
|
||||
|
||||
expect(result).toEqual({ success: true, action: 'upsert' });
|
||||
expect(mockUserFindFirst).not.toHaveBeenCalled();
|
||||
expect(mockUserUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects a change to an email owned by another user', async () => {
|
||||
mockProfileFindUnique.mockResolvedValue({
|
||||
id: 'p1',
|
||||
userId: 'user-1',
|
||||
email: 'jane@example.com',
|
||||
user: { email: 'old@example.com' }
|
||||
});
|
||||
mockUserFindFirst.mockResolvedValue({ id: 'user-2' });
|
||||
|
||||
const request = makeProfileRequest({ email: 'taken@example.com', id: 'p1' });
|
||||
const result = await actions.upsert({ request } as any);
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
action: 'upsert',
|
||||
error: 'That email already belongs to another user.'
|
||||
});
|
||||
expect(mockUserUpdate).not.toHaveBeenCalled();
|
||||
expect(mockProfileUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('links the unlinked profile when a user matches the new email', async () => {
|
||||
mockProfileFindUnique.mockResolvedValue({
|
||||
id: 'p1',
|
||||
userId: null,
|
||||
email: 'jane@example.com',
|
||||
user: null
|
||||
});
|
||||
mockUserFindUnique.mockResolvedValue({ id: 'user-9' });
|
||||
mockProfileUpdate.mockResolvedValue({});
|
||||
|
||||
const request = makeProfileRequest({ id: 'p1' });
|
||||
const result = await actions.upsert({ request } as any);
|
||||
|
||||
expect(result).toEqual({ success: true, action: 'upsert' });
|
||||
expect(mockProfileUpdate).toHaveBeenCalledWith({
|
||||
where: { id: 'p1' },
|
||||
data: { ...validFields, userId: 'user-9' }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('upsert - validation', () => {
|
||||
it('returns field errors for invalid input', async () => {
|
||||
const request = makeProfileRequest({ email: 'not-an-email' });
|
||||
const result = await actions.upsert({ request } as any);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.action).toBe('upsert');
|
||||
expect((result as { fieldErrors: Record<string, string> }).fieldErrors.email).toBeDefined();
|
||||
});
|
||||
|
||||
it('returns a record-not-found error for a missing profile', async () => {
|
||||
mockProfileFindUnique.mockResolvedValue(null);
|
||||
|
||||
const request = makeProfileRequest({ id: 'missing' });
|
||||
const result = await actions.upsert({ request } as any);
|
||||
|
||||
expect(result).toEqual({ success: false, action: 'upsert', error: 'Record not found' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('deletes the profile by id', async () => {
|
||||
mockProfileDelete.mockResolvedValue({});
|
||||
|
||||
const locals = createLocals();
|
||||
const req = createFormRequest({ id: 'p1' });
|
||||
const result = await actions.delete({ request: req, locals } as any);
|
||||
|
||||
expect(result).toEqual({ success: true, action: 'delete' });
|
||||
expect(mockProfileDelete).toHaveBeenCalledWith({ where: { id: 'p1' } });
|
||||
});
|
||||
|
||||
it('returns an error when id is missing', async () => {
|
||||
const locals = createLocals();
|
||||
const result = await actions.delete({ request: createFormRequest({}), locals } as any);
|
||||
|
||||
expect(result).toEqual({ success: false, error: 'Missing id' });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,18 +1,70 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getModelConfig } from '$lib/svelte-admin/registry';
|
||||
import { loadRecords, upsertRecord, deleteRecord } from '$lib/svelte-admin/crud';
|
||||
import { prisma } from '$lib/db';
|
||||
import { superValidate } from 'sveltekit-superforms/server';
|
||||
import { zod4 } from 'sveltekit-superforms/adapters';
|
||||
import { z } from 'zod';
|
||||
import { superFormErrorsToFieldErrors } from '$lib/superform-errors';
|
||||
|
||||
const config = getModelConfig('solfeggio-frequencies')!;
|
||||
const SolfeggioSchema = z.object({
|
||||
name: z.string().min(1, 'Required').max(100, 'Too long'),
|
||||
value: z.coerce.number().int().min(1, 'Value must be a positive integer')
|
||||
});
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
return loadRecords(locals, config);
|
||||
export const load: PageServerLoad = async () => {
|
||||
const records = await prisma.solfeggioFrequency.findMany({ orderBy: { id: 'asc' } });
|
||||
return { records: records as unknown as Record<string, unknown>[] };
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
upsert: async ({ request, locals }) => {
|
||||
return upsertRecord(request, config, locals);
|
||||
upsert: async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
const form = await superValidate(formData, zod4(SolfeggioSchema));
|
||||
if (!form.valid) {
|
||||
const { error: formError, fieldErrors } = superFormErrorsToFieldErrors(
|
||||
form.errors as Record<string, unknown>
|
||||
);
|
||||
return { success: false, action: 'upsert', error: formError, fieldErrors };
|
||||
}
|
||||
|
||||
const rawId = formData.get('id')?.toString();
|
||||
const data = { name: form.data.name, value: form.data.value };
|
||||
|
||||
try {
|
||||
if (rawId) {
|
||||
await prisma.solfeggioFrequency.update({ where: { id: Number(rawId) }, data });
|
||||
} else {
|
||||
await prisma.solfeggioFrequency.create({ data });
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2002') {
|
||||
return {
|
||||
success: false,
|
||||
action: 'upsert',
|
||||
error: 'A frequency with that name or value already exists.'
|
||||
};
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
return { success: true, action: 'upsert' };
|
||||
},
|
||||
delete: async ({ request, locals }) => {
|
||||
return deleteRecord(request, config, locals);
|
||||
delete: async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
const rawId = formData.get('id')?.toString();
|
||||
if (!rawId) return { success: false, error: 'Missing id' };
|
||||
|
||||
try {
|
||||
await prisma.solfeggioFrequency.delete({ where: { id: Number(rawId) } });
|
||||
} catch (e: unknown) {
|
||||
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2003') {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Cannot delete: this frequency is used by proclamations.'
|
||||
};
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
return { success: true, action: 'delete' };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,90 +1,167 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { PageProps } from './$types';
|
||||
import type { ActionResult } from '$lib/svelte-admin/types';
|
||||
import AdminTable from '$lib/components/admin/AdminTable.svelte';
|
||||
import DataTable, { type DataTableColumn } from '$lib/components/DataTable.svelte';
|
||||
import AdminModal from '$lib/components/admin/AdminModal.svelte';
|
||||
import Toast from '$lib/components/admin/Toast.svelte';
|
||||
import { getModelConfig } from '$lib/svelte-admin/registry';
|
||||
import FreqForm from './FreqForm.svelte';
|
||||
|
||||
type ActionResult = {
|
||||
success?: boolean;
|
||||
action?: string;
|
||||
error?: string;
|
||||
fieldErrors?: Record<string, string>;
|
||||
} | null;
|
||||
|
||||
let { data, form }: PageProps = $props();
|
||||
let canDelete = $derived((data.adminUser?.roleId ?? 4) <= 2);
|
||||
|
||||
const config = getModelConfig('solfeggio-frequencies')!;
|
||||
const canDelete = $derived((data.adminUser?.roleId ?? 4) <= 2);
|
||||
const columns: DataTableColumn[] = [
|
||||
{ label: 'Name', value: (r) => r.name },
|
||||
{ label: 'Value', value: (r) => r.value }
|
||||
];
|
||||
|
||||
let modalOpen = $state(false);
|
||||
let editingRecord = $state<Record<string, unknown> | null>(null);
|
||||
let toastMessage = $state('');
|
||||
let toastType = $state<'success' | 'error'>('success');
|
||||
let fieldErrors = $state<Record<string, string>>({});
|
||||
|
||||
let name = $state('');
|
||||
let value = $state('');
|
||||
let formError = $state('');
|
||||
let fieldErrors = $state<Record<string, string>>({});
|
||||
let successMessage = $state('');
|
||||
let lastForm = $state<ActionResult>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (form && 'success' in (form as any)) {
|
||||
const result = form as any;
|
||||
if (result.success) {
|
||||
toastMessage = 'Saved successfully';
|
||||
toastType = 'success';
|
||||
closeModal();
|
||||
} else {
|
||||
formError = result.error ?? '';
|
||||
fieldErrors = result.fieldErrors ?? {};
|
||||
}
|
||||
const rec = editingRecord ?? {};
|
||||
name = String(rec.name ?? '');
|
||||
value = String(rec.value ?? '');
|
||||
if (!editingRecord) {
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
successMessage = '';
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!form || form === lastForm) return;
|
||||
lastForm = form;
|
||||
|
||||
if (form.success) {
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
successMessage = editingRecord ? 'Frequency updated' : 'Frequency created';
|
||||
setTimeout(() => {
|
||||
modalOpen = false;
|
||||
successMessage = '';
|
||||
}, 1200);
|
||||
} else {
|
||||
successMessage = '';
|
||||
formError = form.error ?? '';
|
||||
fieldErrors = form.fieldErrors ?? {};
|
||||
}
|
||||
});
|
||||
|
||||
function openCreate() {
|
||||
editingRecord = null;
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
lastForm = null;
|
||||
modalOpen = true;
|
||||
}
|
||||
|
||||
function openEdit(record: Record<string, unknown>) {
|
||||
editingRecord = record;
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
lastForm = null;
|
||||
modalOpen = true;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modalOpen = false;
|
||||
editingRecord = null;
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<h1 class="mb-4 text-xl font-bold text-gray-900 dark:text-gray-100">{config.displayName}</h1>
|
||||
<AdminTable
|
||||
<div>
|
||||
<DataTable
|
||||
title="Solfeggio Frequencies"
|
||||
{columns}
|
||||
records={data.records}
|
||||
listFields={config.listFields}
|
||||
searchFields={config.searchFields}
|
||||
fieldTypes={config.fieldTypes}
|
||||
singular={config.displayNameSingular}
|
||||
newLabel="New Frequency"
|
||||
searchPlaceholder="Search frequencies..."
|
||||
onRowClick={openEdit}
|
||||
onNew={openCreate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AdminModal
|
||||
title={editingRecord
|
||||
? `Edit ${config.displayNameSingular}`
|
||||
: `Create ${config.displayNameSingular}`}
|
||||
open={modalOpen}
|
||||
onClose={closeModal}
|
||||
{canDelete}
|
||||
recordId={editingRecord?.id as string | undefined}
|
||||
>
|
||||
<FreqForm
|
||||
record={editingRecord ?? {}}
|
||||
{fieldErrors}
|
||||
{formError}
|
||||
submitLabel={editingRecord ? 'Update' : 'Create'}
|
||||
/>
|
||||
</AdminModal>
|
||||
{#if modalOpen}
|
||||
<AdminModal
|
||||
title={editingRecord ? 'Edit Frequency' : 'Create Frequency'}
|
||||
open={modalOpen}
|
||||
onClose={closeModal}
|
||||
{canDelete}
|
||||
recordId={editingRecord?.id as string | undefined}
|
||||
>
|
||||
{#if successMessage}
|
||||
<div
|
||||
class="mb-4 rounded-md border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700"
|
||||
>
|
||||
{successMessage}
|
||||
</div>
|
||||
{/if}
|
||||
{#if formError}
|
||||
<div class="mb-4 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{formError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if toastMessage}
|
||||
<Toast message={toastMessage} type={toastType} />
|
||||
<form method="POST" action="?/upsert" use:enhance class="flex flex-col gap-4">
|
||||
{#if editingRecord}
|
||||
<input type="hidden" name="id" value={String(editingRecord.id)} />
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<label for="field-name" class="mb-1 block text-sm font-medium text-slate-700">Name</label>
|
||||
<input
|
||||
id="field-name"
|
||||
type="text"
|
||||
name="name"
|
||||
required
|
||||
bind:value={name}
|
||||
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-indigo-500 focus:outline-none"
|
||||
/>
|
||||
{#if fieldErrors.name}
|
||||
<p class="mt-1 text-sm text-red-600">{fieldErrors.name}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="field-value" class="mb-1 block text-sm font-medium text-slate-700">Value</label>
|
||||
<input
|
||||
id="field-value"
|
||||
type="number"
|
||||
name="value"
|
||||
required
|
||||
min="1"
|
||||
step="1"
|
||||
bind:value
|
||||
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-indigo-500 focus:outline-none"
|
||||
/>
|
||||
{#if fieldErrors.value}
|
||||
<p class="mt-1 text-sm text-red-600">{fieldErrors.value}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onclick={closeModal}
|
||||
class="rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-indigo-700"
|
||||
>
|
||||
{editingRecord ? 'Save Frequency' : 'Create Frequency'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AdminModal>
|
||||
{/if}
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
|
||||
let {
|
||||
record = {} as Record<string, unknown>,
|
||||
fieldErrors = {} as Record<string, string>,
|
||||
formError = '',
|
||||
action = '?/upsert',
|
||||
submitLabel = 'Save'
|
||||
}: {
|
||||
record?: Record<string, unknown>;
|
||||
fieldErrors?: Record<string, string>;
|
||||
formError?: string;
|
||||
action?: string;
|
||||
submitLabel?: string;
|
||||
} = $props();
|
||||
|
||||
let name = $state(String(record.name ?? ''));
|
||||
let value = $state(String(record.value ?? ''));
|
||||
let dirty = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
dirty = false;
|
||||
});
|
||||
|
||||
function markDirty() {
|
||||
if (record.id) dirty = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<form {action} method="POST" use:enhance oninput={markDirty}>
|
||||
{#if record?.id}
|
||||
<input type="hidden" name="id" value={String(record.id)} />
|
||||
{/if}
|
||||
|
||||
{#if formError}
|
||||
<div class="mb-4 border border-red-300 bg-red-50 px-4 py-2 text-sm text-red-700">
|
||||
{formError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<label
|
||||
for="field-name"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="field-name"
|
||||
name="name"
|
||||
bind:value={name}
|
||||
class="w-full border px-3 py-1.5 text-sm {fieldErrors.name
|
||||
? 'border-red-400'
|
||||
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
{#if fieldErrors.name}
|
||||
<p class="mt-1 text-xs text-red-600">{fieldErrors.name}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="field-value"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Value
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="field-value"
|
||||
name="value"
|
||||
bind:value
|
||||
step="1"
|
||||
class="w-full border px-3 py-1.5 text-sm {fieldErrors.value
|
||||
? 'border-red-400'
|
||||
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
{#if fieldErrors.value}
|
||||
<p class="mt-1 text-xs text-red-600">{fieldErrors.value}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!!record?.id && !dirty}
|
||||
class="border border-gray-300 bg-gray-100 px-4 py-1.5 text-sm font-medium hover:bg-gray-200 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
>
|
||||
{submitLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getModelConfig } from '$lib/svelte-admin/registry';
|
||||
import { loadRecords, upsertRecord, deleteRecord } from '$lib/svelte-admin/crud';
|
||||
|
||||
const config = getModelConfig('subscription-levels')!;
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
return loadRecords(locals, config);
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
upsert: async ({ request, locals }) => {
|
||||
return upsertRecord(request, config, locals);
|
||||
},
|
||||
delete: async ({ request, locals }) => {
|
||||
return deleteRecord(request, config, locals);
|
||||
}
|
||||
};
|
||||
@@ -1,90 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { PageProps } from './$types';
|
||||
import type { ActionResult } from '$lib/svelte-admin/types';
|
||||
import AdminTable from '$lib/components/admin/AdminTable.svelte';
|
||||
import AdminModal from '$lib/components/admin/AdminModal.svelte';
|
||||
import Toast from '$lib/components/admin/Toast.svelte';
|
||||
import { getModelConfig } from '$lib/svelte-admin/registry';
|
||||
import LevelForm from './LevelForm.svelte';
|
||||
|
||||
let { data, form }: PageProps = $props();
|
||||
|
||||
const config = getModelConfig('subscription-levels')!;
|
||||
const canDelete = $derived((data.adminUser?.roleId ?? 4) <= 2);
|
||||
|
||||
let modalOpen = $state(false);
|
||||
let editingRecord = $state<Record<string, unknown> | null>(null);
|
||||
let toastMessage = $state('');
|
||||
let toastType = $state<'success' | 'error'>('success');
|
||||
let fieldErrors = $state<Record<string, string>>({});
|
||||
let formError = $state('');
|
||||
|
||||
$effect(() => {
|
||||
if (form && 'success' in (form as any)) {
|
||||
const result = form as any;
|
||||
if (result.success) {
|
||||
toastMessage = 'Saved successfully';
|
||||
toastType = 'success';
|
||||
closeModal();
|
||||
} else {
|
||||
formError = result.error ?? '';
|
||||
fieldErrors = result.fieldErrors ?? {};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function openCreate() {
|
||||
editingRecord = null;
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
modalOpen = true;
|
||||
}
|
||||
|
||||
function openEdit(record: Record<string, unknown>) {
|
||||
editingRecord = record;
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
modalOpen = true;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modalOpen = false;
|
||||
editingRecord = null;
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<h1 class="mb-4 text-xl font-bold text-gray-900 dark:text-gray-100">{config.displayName}</h1>
|
||||
<AdminTable
|
||||
records={data.records}
|
||||
listFields={config.listFields}
|
||||
searchFields={config.searchFields}
|
||||
fieldTypes={config.fieldTypes}
|
||||
singular={config.displayNameSingular}
|
||||
onRowClick={openEdit}
|
||||
onNew={openCreate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AdminModal
|
||||
title={editingRecord
|
||||
? `Edit ${config.displayNameSingular}`
|
||||
: `Create ${config.displayNameSingular}`}
|
||||
open={modalOpen}
|
||||
onClose={closeModal}
|
||||
{canDelete}
|
||||
recordId={editingRecord?.id as string | undefined}
|
||||
>
|
||||
<LevelForm
|
||||
record={editingRecord ?? {}}
|
||||
{fieldErrors}
|
||||
{formError}
|
||||
submitLabel={editingRecord ? 'Update' : 'Create'}
|
||||
/>
|
||||
</AdminModal>
|
||||
|
||||
{#if toastMessage}
|
||||
<Toast message={toastMessage} type={toastType} />
|
||||
{/if}
|
||||
@@ -1,95 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
|
||||
let {
|
||||
record = {} as Record<string, unknown>,
|
||||
fieldErrors = {} as Record<string, string>,
|
||||
formError = '',
|
||||
action = '?/upsert',
|
||||
submitLabel = 'Save'
|
||||
}: {
|
||||
record?: Record<string, unknown>;
|
||||
fieldErrors?: Record<string, string>;
|
||||
formError?: string;
|
||||
action?: string;
|
||||
submitLabel?: string;
|
||||
} = $props();
|
||||
|
||||
let name = $state(String(record.name ?? ''));
|
||||
let description = $state(String(record.description ?? ''));
|
||||
let dirty = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
dirty = false;
|
||||
});
|
||||
|
||||
function markDirty() {
|
||||
if (record.id) dirty = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<form {action} method="POST" use:enhance oninput={markDirty}>
|
||||
{#if record?.id}
|
||||
<input type="hidden" name="id" value={String(record.id)} />
|
||||
{/if}
|
||||
|
||||
{#if formError}
|
||||
<div class="mb-4 border border-red-300 bg-red-50 px-4 py-2 text-sm text-red-700">
|
||||
{formError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<label
|
||||
for="field-name"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="field-name"
|
||||
name="name"
|
||||
bind:value={name}
|
||||
class="w-full border px-3 py-1.5 text-sm {fieldErrors.name
|
||||
? 'border-red-400'
|
||||
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
{#if fieldErrors.name}
|
||||
<p class="mt-1 text-xs text-red-600">{fieldErrors.name}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="field-description"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Description
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="field-description"
|
||||
name="description"
|
||||
bind:value={description}
|
||||
class="w-full border px-3 py-1.5 text-sm {fieldErrors.description
|
||||
? 'border-red-400'
|
||||
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
{#if fieldErrors.description}
|
||||
<p class="mt-1 text-xs text-red-600">{fieldErrors.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!!record?.id && !dirty}
|
||||
class="border border-gray-300 bg-gray-100 px-4 py-1.5 text-sm font-medium hover:bg-gray-200 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
>
|
||||
{submitLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,18 +1,62 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getModelConfig } from '$lib/svelte-admin/registry';
|
||||
import { loadRecords, upsertRecord, deleteRecord } from '$lib/svelte-admin/crud';
|
||||
import { prisma } from '$lib/db';
|
||||
import { superValidate } from 'sveltekit-superforms/server';
|
||||
import { zod4 } from 'sveltekit-superforms/adapters';
|
||||
import { z } from 'zod';
|
||||
import { superFormErrorsToFieldErrors } from '$lib/superform-errors';
|
||||
|
||||
const config = getModelConfig('user-roles')!;
|
||||
const UserRoleSchema = z.object({
|
||||
name: z.string().min(1, 'Required').max(100, 'Too long')
|
||||
});
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
return loadRecords(locals, config);
|
||||
export const load: PageServerLoad = async () => {
|
||||
const records = await prisma.userRole.findMany({ orderBy: { id: 'asc' } });
|
||||
return { records: records as unknown as Record<string, unknown>[] };
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
upsert: async ({ request, locals }) => {
|
||||
return upsertRecord(request, config, locals);
|
||||
upsert: async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
const form = await superValidate(formData, zod4(UserRoleSchema));
|
||||
if (!form.valid) {
|
||||
const { error: formError, fieldErrors } = superFormErrorsToFieldErrors(
|
||||
form.errors as Record<string, unknown>
|
||||
);
|
||||
return { success: false, action: 'upsert', error: formError, fieldErrors };
|
||||
}
|
||||
|
||||
const rawId = formData.get('id')?.toString();
|
||||
const data = { name: form.data.name };
|
||||
|
||||
try {
|
||||
if (rawId) {
|
||||
await prisma.userRole.update({ where: { id: Number(rawId) }, data });
|
||||
} else {
|
||||
await prisma.userRole.create({ data });
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2002') {
|
||||
return { success: false, action: 'upsert', error: 'A role with that name already exists.' };
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
return { success: true, action: 'upsert' };
|
||||
},
|
||||
delete: async ({ request, locals }) => {
|
||||
return deleteRecord(request, config, locals);
|
||||
delete: async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
const rawId = formData.get('id')?.toString();
|
||||
if (!rawId) return { success: false, error: 'Missing id' };
|
||||
|
||||
try {
|
||||
await prisma.userRole.delete({ where: { id: Number(rawId) } });
|
||||
} catch (e: unknown) {
|
||||
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2003') {
|
||||
return { success: false, error: 'Cannot delete: this role is assigned to users.' };
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
return { success: true, action: 'delete' };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,90 +1,148 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { PageProps } from './$types';
|
||||
import type { ActionResult } from '$lib/svelte-admin/types';
|
||||
import AdminTable from '$lib/components/admin/AdminTable.svelte';
|
||||
import DataTable, { type DataTableColumn } from '$lib/components/DataTable.svelte';
|
||||
import AdminModal from '$lib/components/admin/AdminModal.svelte';
|
||||
import Toast from '$lib/components/admin/Toast.svelte';
|
||||
import { getModelConfig } from '$lib/svelte-admin/registry';
|
||||
import UserRoleForm from './UserRoleForm.svelte';
|
||||
|
||||
type ActionResult = {
|
||||
success?: boolean;
|
||||
action?: string;
|
||||
error?: string;
|
||||
fieldErrors?: Record<string, string>;
|
||||
} | null;
|
||||
|
||||
let { data, form }: PageProps = $props();
|
||||
let canDelete = $derived((data.adminUser?.roleId ?? 4) <= 2);
|
||||
|
||||
const config = getModelConfig('user-roles')!;
|
||||
const canDelete = $derived((data.adminUser?.roleId ?? 4) <= 2);
|
||||
const columns: DataTableColumn[] = [
|
||||
{ label: 'Id', value: (r) => r.id },
|
||||
{ label: 'Name', value: (r) => r.name }
|
||||
];
|
||||
|
||||
let modalOpen = $state(false);
|
||||
let editingRecord = $state<Record<string, unknown> | null>(null);
|
||||
let toastMessage = $state('');
|
||||
let toastType = $state<'success' | 'error'>('success');
|
||||
let fieldErrors = $state<Record<string, string>>({});
|
||||
|
||||
let name = $state('');
|
||||
let formError = $state('');
|
||||
let fieldErrors = $state<Record<string, string>>({});
|
||||
let successMessage = $state('');
|
||||
let lastForm = $state<ActionResult>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (form && 'success' in (form as any)) {
|
||||
const result = form as any;
|
||||
if (result.success) {
|
||||
toastMessage = 'Saved successfully';
|
||||
toastType = 'success';
|
||||
closeModal();
|
||||
} else {
|
||||
formError = result.error ?? '';
|
||||
fieldErrors = result.fieldErrors ?? {};
|
||||
}
|
||||
const rec = editingRecord ?? {};
|
||||
name = String(rec.name ?? '');
|
||||
if (!editingRecord) {
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
successMessage = '';
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!form || form === lastForm) return;
|
||||
lastForm = form;
|
||||
|
||||
if (form.success) {
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
successMessage = editingRecord ? 'Role updated' : 'Role created';
|
||||
setTimeout(() => {
|
||||
modalOpen = false;
|
||||
successMessage = '';
|
||||
}, 1200);
|
||||
} else {
|
||||
successMessage = '';
|
||||
formError = form.error ?? '';
|
||||
fieldErrors = form.fieldErrors ?? {};
|
||||
}
|
||||
});
|
||||
|
||||
function openCreate() {
|
||||
editingRecord = null;
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
lastForm = null;
|
||||
modalOpen = true;
|
||||
}
|
||||
|
||||
function openEdit(record: Record<string, unknown>) {
|
||||
editingRecord = record;
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
lastForm = null;
|
||||
modalOpen = true;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modalOpen = false;
|
||||
editingRecord = null;
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<h1 class="mb-4 text-xl font-bold text-gray-900 dark:text-gray-100">{config.displayName}</h1>
|
||||
<AdminTable
|
||||
<div>
|
||||
<DataTable
|
||||
title="User Roles"
|
||||
{columns}
|
||||
records={data.records}
|
||||
listFields={config.listFields}
|
||||
searchFields={config.searchFields}
|
||||
fieldTypes={config.fieldTypes}
|
||||
singular={config.displayNameSingular}
|
||||
newLabel="New Role"
|
||||
searchPlaceholder="Search roles..."
|
||||
onRowClick={openEdit}
|
||||
onNew={openCreate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AdminModal
|
||||
title={editingRecord
|
||||
? `Edit ${config.displayNameSingular}`
|
||||
: `Create ${config.displayNameSingular}`}
|
||||
open={modalOpen}
|
||||
onClose={closeModal}
|
||||
{canDelete}
|
||||
recordId={editingRecord?.id as string | undefined}
|
||||
>
|
||||
<UserRoleForm
|
||||
record={editingRecord ?? {}}
|
||||
{fieldErrors}
|
||||
{formError}
|
||||
submitLabel={editingRecord ? 'Update' : 'Create'}
|
||||
/>
|
||||
</AdminModal>
|
||||
{#if modalOpen}
|
||||
<AdminModal
|
||||
title={editingRecord ? 'Edit User Role' : 'Create User Role'}
|
||||
open={modalOpen}
|
||||
onClose={closeModal}
|
||||
{canDelete}
|
||||
recordId={editingRecord?.id as string | undefined}
|
||||
>
|
||||
{#if successMessage}
|
||||
<div
|
||||
class="mb-4 rounded-md border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700"
|
||||
>
|
||||
{successMessage}
|
||||
</div>
|
||||
{/if}
|
||||
{#if formError}
|
||||
<div class="mb-4 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{formError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if toastMessage}
|
||||
<Toast message={toastMessage} type={toastType} />
|
||||
<form method="POST" action="?/upsert" use:enhance class="flex flex-col gap-4">
|
||||
{#if editingRecord}
|
||||
<input type="hidden" name="id" value={String(editingRecord.id)} />
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<label for="field-name" class="mb-1 block text-sm font-medium text-slate-700">Name</label>
|
||||
<input
|
||||
id="field-name"
|
||||
type="text"
|
||||
name="name"
|
||||
required
|
||||
bind:value={name}
|
||||
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-indigo-500 focus:outline-none"
|
||||
/>
|
||||
{#if fieldErrors.name}
|
||||
<p class="mt-1 text-sm text-red-600">{fieldErrors.name}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onclick={closeModal}
|
||||
class="rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-indigo-700"
|
||||
>
|
||||
{editingRecord ? 'Save Role' : 'Create Role'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AdminModal>
|
||||
{/if}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
|
||||
let {
|
||||
record = {} as Record<string, unknown>,
|
||||
fieldErrors = {} as Record<string, string>,
|
||||
formError = '',
|
||||
action = '?/upsert',
|
||||
submitLabel = 'Save'
|
||||
}: {
|
||||
record?: Record<string, unknown>;
|
||||
fieldErrors?: Record<string, string>;
|
||||
formError?: string;
|
||||
action?: string;
|
||||
submitLabel?: string;
|
||||
} = $props();
|
||||
|
||||
let name = $state(String(record.name ?? ''));
|
||||
let dirty = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
dirty = false;
|
||||
});
|
||||
|
||||
function markDirty() {
|
||||
if (record.id) dirty = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<form {action} method="POST" use:enhance oninput={markDirty}>
|
||||
{#if record?.id}
|
||||
<input type="hidden" name="id" value={String(record.id)} />
|
||||
{/if}
|
||||
|
||||
{#if formError}
|
||||
<div class="mb-4 border border-red-300 bg-red-50 px-4 py-2 text-sm text-red-700">
|
||||
{formError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<label
|
||||
for="field-name"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="field-name"
|
||||
name="name"
|
||||
bind:value={name}
|
||||
class="w-full border px-3 py-1.5 text-sm {fieldErrors.name
|
||||
? 'border-red-400'
|
||||
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
{#if fieldErrors.name}
|
||||
<p class="mt-1 text-xs text-red-600">{fieldErrors.name}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!!record?.id && !dirty}
|
||||
class="border border-gray-300 bg-gray-100 px-4 py-1.5 text-sm font-medium hover:bg-gray-200 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
>
|
||||
{submitLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,18 +1,113 @@
|
||||
import type { PageServerLoad, Actions } from './$types';
|
||||
import { getModelConfig } from '$lib/svelte-admin/registry';
|
||||
import { loadRecords, upsertRecord, deleteRecord } from '$lib/svelte-admin/crud';
|
||||
import { prisma } from '$lib/db';
|
||||
import { superValidate } from 'sveltekit-superforms/server';
|
||||
import { zod4 } from 'sveltekit-superforms/adapters';
|
||||
import { z } from 'zod';
|
||||
import { superFormErrorsToFieldErrors } from '$lib/superform-errors';
|
||||
|
||||
const config = getModelConfig('users')!;
|
||||
const UserSchema = z.object({
|
||||
name: z.string().min(1, 'Required').max(100, 'Too long'),
|
||||
email: z.string().email('Invalid email address'),
|
||||
roleId: z.coerce.number().int().min(1)
|
||||
});
|
||||
|
||||
async function myRoleId(locals: App.Locals): Promise<number> {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: locals.user?.id },
|
||||
select: { roleId: true }
|
||||
});
|
||||
return user?.roleId ?? 4;
|
||||
}
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
return loadRecords(locals, config);
|
||||
const roleId = await myRoleId(locals);
|
||||
|
||||
const [users, roles] = await Promise.all([
|
||||
prisma.user.findMany({
|
||||
include: { role: true },
|
||||
orderBy: { createdAt: 'desc' }
|
||||
}),
|
||||
prisma.userRole.findMany({ orderBy: { id: 'asc' } })
|
||||
]);
|
||||
|
||||
const roleOptions = roles.filter((role) => role.id >= roleId);
|
||||
|
||||
return {
|
||||
records: users as unknown as Record<string, unknown>[],
|
||||
roleOptions: roleOptions as unknown as Record<string, unknown>[]
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
upsert: async ({ request, locals }) => {
|
||||
return upsertRecord(request, config, locals);
|
||||
const formData = await request.formData();
|
||||
const form = await superValidate(formData, zod4(UserSchema));
|
||||
if (!form.valid) {
|
||||
const { error: formError, fieldErrors } = superFormErrorsToFieldErrors(
|
||||
form.errors as Record<string, unknown>
|
||||
);
|
||||
return { success: false, action: 'upsert', error: formError, fieldErrors };
|
||||
}
|
||||
|
||||
const rawId = formData.get('id')?.toString();
|
||||
const data = {
|
||||
name: form.data.name,
|
||||
email: form.data.email,
|
||||
roleId: form.data.roleId
|
||||
};
|
||||
|
||||
const myRole = await myRoleId(locals);
|
||||
if (myRole > 1 && data.roleId < myRole) {
|
||||
return {
|
||||
success: false,
|
||||
action: 'upsert',
|
||||
error: 'You cannot assign a role with more privileges than your own.'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
if (rawId) {
|
||||
await prisma.user.update({ where: { id: rawId }, data });
|
||||
} else {
|
||||
await prisma.user.create({ data: { id: crypto.randomUUID(), ...data } });
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2002') {
|
||||
return {
|
||||
success: false,
|
||||
action: 'upsert',
|
||||
error: 'A user with that email already exists.'
|
||||
};
|
||||
}
|
||||
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2003') {
|
||||
return {
|
||||
success: false,
|
||||
action: 'upsert',
|
||||
error: 'That role does not exist.'
|
||||
};
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
return { success: true, action: 'upsert' };
|
||||
},
|
||||
delete: async ({ request, locals }) => {
|
||||
return deleteRecord(request, config, locals);
|
||||
delete: async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
const rawId = formData.get('id')?.toString();
|
||||
if (!rawId) return { success: false, error: 'Missing id' };
|
||||
|
||||
try {
|
||||
await prisma.user.delete({ where: { id: rawId } });
|
||||
} catch (e: unknown) {
|
||||
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2003') {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Cannot delete: this user still has related records.'
|
||||
};
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
return { success: true, action: 'delete' };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,91 +1,202 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { PageProps } from './$types';
|
||||
import type { ActionResult } from '$lib/svelte-admin/types';
|
||||
import AdminTable from '$lib/components/admin/AdminTable.svelte';
|
||||
import DataTable, { type DataTableColumn } from '$lib/components/DataTable.svelte';
|
||||
import AdminModal from '$lib/components/admin/AdminModal.svelte';
|
||||
import Toast from '$lib/components/admin/Toast.svelte';
|
||||
import { getModelConfig } from '$lib/svelte-admin/registry';
|
||||
import UserForm from './UserForm.svelte';
|
||||
|
||||
type ActionResult = {
|
||||
success?: boolean;
|
||||
action?: string;
|
||||
error?: string;
|
||||
fieldErrors?: Record<string, string>;
|
||||
} | null;
|
||||
|
||||
let { data, form }: PageProps = $props();
|
||||
let canDelete = $derived((data.adminUser?.roleId ?? 4) <= 2);
|
||||
|
||||
const config = getModelConfig('users')!;
|
||||
const canDelete = $derived((data.adminUser?.roleId ?? 4) <= 2);
|
||||
const columns: DataTableColumn[] = [
|
||||
{ label: 'Name', value: (r) => r.name },
|
||||
{ label: 'Email', value: (r) => r.email },
|
||||
{
|
||||
label: 'Role',
|
||||
value: (r) => (r.role as Record<string, unknown> | undefined)?.name ?? (r.roleId as string)
|
||||
},
|
||||
{
|
||||
label: 'Created',
|
||||
value: (r) =>
|
||||
r.createdAt
|
||||
? new Date(r.createdAt as string).toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
: ''
|
||||
}
|
||||
];
|
||||
|
||||
let modalOpen = $state(false);
|
||||
let editingRecord = $state<Record<string, unknown> | null>(null);
|
||||
let toastMessage = $state('');
|
||||
let toastType = $state<'success' | 'error'>('success');
|
||||
let fieldErrors = $state<Record<string, string>>({});
|
||||
|
||||
let name = $state('');
|
||||
let email = $state('');
|
||||
let roleId = $state('');
|
||||
|
||||
let formError = $state('');
|
||||
let fieldErrors = $state<Record<string, string>>({});
|
||||
let successMessage = $state('');
|
||||
let lastForm = $state<ActionResult>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (form && 'success' in (form as any)) {
|
||||
const result = form as any;
|
||||
if (result.success) {
|
||||
toastMessage = 'Saved successfully';
|
||||
toastType = 'success';
|
||||
closeModal();
|
||||
} else {
|
||||
formError = result.error ?? '';
|
||||
fieldErrors = result.fieldErrors ?? {};
|
||||
}
|
||||
const rec = editingRecord ?? {};
|
||||
name = String(rec.name ?? '');
|
||||
email = String(rec.email ?? '');
|
||||
roleId = String(rec.roleId ?? '');
|
||||
if (!editingRecord) {
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
successMessage = '';
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!form || form === lastForm) return;
|
||||
lastForm = form;
|
||||
|
||||
if (form.success) {
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
successMessage = editingRecord ? 'User updated' : 'User created';
|
||||
setTimeout(() => {
|
||||
modalOpen = false;
|
||||
successMessage = '';
|
||||
}, 1200);
|
||||
} else {
|
||||
successMessage = '';
|
||||
formError = form.error ?? '';
|
||||
fieldErrors = form.fieldErrors ?? {};
|
||||
}
|
||||
});
|
||||
|
||||
function openCreate() {
|
||||
editingRecord = null;
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
lastForm = null;
|
||||
modalOpen = true;
|
||||
}
|
||||
|
||||
function openEdit(record: Record<string, unknown>) {
|
||||
editingRecord = record;
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
lastForm = null;
|
||||
modalOpen = true;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modalOpen = false;
|
||||
editingRecord = null;
|
||||
formError = '';
|
||||
fieldErrors = {};
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<h1 class="mb-4 text-xl font-bold text-gray-900 dark:text-gray-100">{config.displayName}</h1>
|
||||
<AdminTable
|
||||
<div>
|
||||
<DataTable
|
||||
title="Users"
|
||||
{columns}
|
||||
records={data.records}
|
||||
listFields={config.listFields}
|
||||
searchFields={config.searchFields}
|
||||
fieldTypes={config.fieldTypes}
|
||||
singular={config.displayNameSingular}
|
||||
newLabel="New User"
|
||||
searchPlaceholder="Search users..."
|
||||
onRowClick={openEdit}
|
||||
onNew={config.listOnly ? undefined : openCreate}
|
||||
onNew={openCreate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AdminModal
|
||||
title={editingRecord
|
||||
? `Edit ${config.displayNameSingular}`
|
||||
: `Create ${config.displayNameSingular}`}
|
||||
open={modalOpen}
|
||||
onClose={closeModal}
|
||||
{canDelete}
|
||||
recordId={editingRecord?.id as string | undefined}
|
||||
>
|
||||
<UserForm
|
||||
record={editingRecord ?? {}}
|
||||
relationOptionsData={data.relationOptionsData}
|
||||
{fieldErrors}
|
||||
{formError}
|
||||
submitLabel={editingRecord ? 'Update' : 'Create'}
|
||||
/>
|
||||
</AdminModal>
|
||||
{#if modalOpen}
|
||||
<AdminModal
|
||||
title={editingRecord ? 'Edit User' : 'Create User'}
|
||||
open={modalOpen}
|
||||
onClose={closeModal}
|
||||
{canDelete}
|
||||
recordId={editingRecord?.id as string | undefined}
|
||||
>
|
||||
{#if successMessage}
|
||||
<div
|
||||
class="mb-4 rounded-md border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700"
|
||||
>
|
||||
{successMessage}
|
||||
</div>
|
||||
{/if}
|
||||
{#if formError}
|
||||
<div class="mb-4 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{formError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if toastMessage}
|
||||
<Toast message={toastMessage} type={toastType} />
|
||||
<form method="POST" action="?/upsert" use:enhance class="flex flex-col gap-4">
|
||||
{#if editingRecord}
|
||||
<input type="hidden" name="id" value={String(editingRecord.id)} />
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<label for="field-name" class="mb-1 block text-sm font-medium text-slate-700">Name</label>
|
||||
<input
|
||||
id="field-name"
|
||||
type="text"
|
||||
name="name"
|
||||
required
|
||||
bind:value={name}
|
||||
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-indigo-500 focus:outline-none"
|
||||
/>
|
||||
{#if fieldErrors.name}
|
||||
<p class="mt-1 text-sm text-red-600">{fieldErrors.name}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="field-email" class="mb-1 block text-sm font-medium text-slate-700">Email</label>
|
||||
<input
|
||||
id="field-email"
|
||||
type="email"
|
||||
name="email"
|
||||
required
|
||||
bind:value={email}
|
||||
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-indigo-500 focus:outline-none"
|
||||
/>
|
||||
{#if fieldErrors.email}
|
||||
<p class="mt-1 text-sm text-red-600">{fieldErrors.email}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="field-roleId" class="mb-1 block text-sm font-medium text-slate-700">Role</label>
|
||||
<select
|
||||
id="field-roleId"
|
||||
name="roleId"
|
||||
required
|
||||
bind:value={roleId}
|
||||
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-indigo-500 focus:outline-none"
|
||||
>
|
||||
<option value="">-- Select Role --</option>
|
||||
{#each data.roleOptions as role (String(role.id))}
|
||||
<option value={String(role.id)}>{role.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if fieldErrors.roleId}
|
||||
<p class="mt-1 text-sm text-red-600">{fieldErrors.roleId}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onclick={closeModal}
|
||||
class="rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-indigo-700"
|
||||
>
|
||||
{editingRecord ? 'Save User' : 'Create User'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AdminModal>
|
||||
{/if}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
|
||||
let {
|
||||
record = {} as Record<string, unknown>,
|
||||
fieldErrors = {} as Record<string, string>,
|
||||
formError = '',
|
||||
relationOptionsData = {} as Record<string, { label: string; value: string | number }[]>,
|
||||
action = '?/upsert',
|
||||
submitLabel = 'Save'
|
||||
}: {
|
||||
record?: Record<string, unknown>;
|
||||
fieldErrors?: Record<string, string>;
|
||||
formError?: string;
|
||||
relationOptionsData?: Record<string, { label: string; value: string | number }[]>;
|
||||
action?: string;
|
||||
submitLabel?: string;
|
||||
} = $props();
|
||||
|
||||
let roleId = $state(String(record.roleId ?? ''));
|
||||
let dirty = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
dirty = false;
|
||||
});
|
||||
|
||||
function markDirty() {
|
||||
if (record.id) dirty = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<form {action} method="POST" use:enhance oninput={markDirty}>
|
||||
{#if record?.id}
|
||||
<input type="hidden" name="id" value={String(record.id)} />
|
||||
{/if}
|
||||
|
||||
{#if formError}
|
||||
<div class="mb-4 border border-red-300 bg-red-50 px-4 py-2 text-sm text-red-700">
|
||||
{formError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<label
|
||||
for="field-roleId"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Role
|
||||
</label>
|
||||
<select
|
||||
id="field-roleId"
|
||||
name="roleId"
|
||||
bind:value={roleId}
|
||||
class="w-full border px-3 py-1.5 text-sm {fieldErrors.roleId
|
||||
? 'border-red-400'
|
||||
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
|
||||
>
|
||||
<option value="">-- Select Role --</option>
|
||||
{#each relationOptionsData.roleId ?? [] as opt (String(opt.value))}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if fieldErrors.roleId}
|
||||
<p class="mt-1 text-xs text-red-600">{fieldErrors.roleId}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!!record?.id && !dirty}
|
||||
class="border border-gray-300 bg-gray-100 px-4 py-1.5 text-sm font-medium hover:bg-gray-200 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
>
|
||||
{submitLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -40,7 +40,7 @@ describe('(protected) layout server load', () => {
|
||||
});
|
||||
|
||||
it('redirects to /account/setup when user has no profile', async () => {
|
||||
findUnique.mockResolvedValue({ id: 'user-1', roleId: null, profile: null });
|
||||
findUnique.mockResolvedValue({ id: 'user-1', roleId: null, profiles: [] });
|
||||
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const url = new URL('http://localhost/dashboard');
|
||||
@@ -53,7 +53,7 @@ describe('(protected) layout server load', () => {
|
||||
});
|
||||
|
||||
it('skips redirect when already on /account/setup', async () => {
|
||||
findUnique.mockResolvedValue({ id: 'user-1', roleId: null, profile: null });
|
||||
findUnique.mockResolvedValue({ id: 'user-1', roleId: null, profiles: [] });
|
||||
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const url = new URL('http://localhost/account/setup');
|
||||
|
||||
@@ -5,12 +5,9 @@
|
||||
|
||||
<Window>
|
||||
{#if $page.status === 403}
|
||||
<h1 class="dark:text-gray-100">Not Authorized</h1>
|
||||
<p class="dark:text-gray-300">
|
||||
You don't have permission to view this page. <a
|
||||
href="/"
|
||||
class="dark:text-gray-400 dark:hover:text-gray-200">Go home</a
|
||||
>.
|
||||
<h1>Not Authorized</h1>
|
||||
<p>
|
||||
You don't have permission to view this page. <a href="/">Go home</a>.
|
||||
</p>
|
||||
{/if}
|
||||
</Window>
|
||||
|
||||
+355
-57
@@ -1,79 +1,377 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { authClient } from '$lib/auth-client';
|
||||
import { canAccessAdmin } from '$lib/svelte-admin/roles';
|
||||
import './layout.css';
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
import { page } from '$app/state';
|
||||
import { resolve } from '$app/paths';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { LayoutData } from './$types';
|
||||
import './layout.css';
|
||||
|
||||
let { data, children } = $props();
|
||||
|
||||
let { children, data }: { children: Snippet; data: LayoutData } = $props();
|
||||
const session = authClient.useSession();
|
||||
let isAdmin = $derived(canAccessAdmin(data.userRoleId));
|
||||
|
||||
let dark = $state(false);
|
||||
let menuOpen = $state(false);
|
||||
let adminOpen = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
dark = document.documentElement.classList.contains('dark');
|
||||
});
|
||||
const isAdmin = $derived((data.userRoleId ?? 4) <= 3);
|
||||
const adminModels = [
|
||||
{ slug: 'users', displayName: 'Users' },
|
||||
{ slug: 'profiles', displayName: 'Profiles' },
|
||||
{ slug: 'user-roles', displayName: 'User Roles' },
|
||||
{ slug: 'proclamations', displayName: 'Proclamations' },
|
||||
{ slug: 'solfeggio-frequencies', displayName: 'Solfeggio Frequencies' },
|
||||
{ slug: 'proclamation-categories', displayName: 'Proclamation Categories' },
|
||||
{ slug: 'proclamation-groups', displayName: 'Proclamation Groups' },
|
||||
{ slug: 'license-keys', displayName: 'License Keys' }
|
||||
];
|
||||
const currentPath = $derived(page.url.pathname);
|
||||
|
||||
function toggleTheme() {
|
||||
dark = !dark;
|
||||
if (dark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('theme', 'light');
|
||||
}
|
||||
function isActive(path: string): boolean {
|
||||
if (path === '/') return currentPath === '/';
|
||||
return currentPath === path || currentPath.startsWith(`${path}/`);
|
||||
}
|
||||
|
||||
function activeClass(path: string) {
|
||||
return isActive(path)
|
||||
? 'bg-indigo-50 text-indigo-700'
|
||||
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900';
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
menuOpen = false;
|
||||
adminOpen = false;
|
||||
}
|
||||
|
||||
async function handleSignOut() {
|
||||
await authClient.signOut();
|
||||
closeMenu();
|
||||
window.location.href = resolve('/');
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="icon" href={favicon} />
|
||||
<title>Rhema Wave Technologies</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Rhema Wave Technologies User Portal — manage proclamations, audio content, and solfeggio frequencies."
|
||||
/>
|
||||
<meta name="theme-color" content="#6b7280" />
|
||||
<meta property="og:title" content="Rhema Wave Technologies" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Manage proclamations, audio content, and solfeggio frequencies."
|
||||
/>
|
||||
<title>Radiant Wave User Portal</title>
|
||||
<meta name="description" content="Radiant Wave Technologies User Portal" />
|
||||
<meta property="og:title" content="Radiant Wave User Portal" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Rhema Wave Technologies" />
|
||||
<meta property="og:site_name" content="Radiant Wave User Portal" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
</svelte:head>
|
||||
|
||||
<nav
|
||||
class="flex w-full items-center justify-between border-b border-gray-200 px-8 py-6 dark:border-gray-700 dark:bg-gray-950 dark:text-gray-100"
|
||||
>
|
||||
<span><a href="/" class="dark:text-gray-100">Rhema Wave Technologies</a></span>
|
||||
<div class="flex items-center gap-4">
|
||||
<button
|
||||
onclick={toggleTheme}
|
||||
class="border border-gray-300 px-3 py-1 text-sm dark:border-gray-600 dark:text-gray-300"
|
||||
>
|
||||
{dark ? 'Light' : 'Dark'}
|
||||
</button>
|
||||
{#if $session.data}
|
||||
<a href="/dashboard" class="dark:text-gray-300 dark:hover:text-gray-100">Dashboard</a>
|
||||
<button
|
||||
class="dark:text-gray-300 dark:hover:text-gray-100"
|
||||
onclick={async () => {
|
||||
await authClient.signOut();
|
||||
window.location.href = '/';
|
||||
}}>Log Out</button
|
||||
<header class="sticky top-0 z-40 border-b border-slate-200 bg-white/90 backdrop-blur">
|
||||
<div class="mx-auto flex h-14 max-w-6xl items-center justify-between gap-4 px-4">
|
||||
<a href={resolve('/')} class="flex shrink-0 items-center gap-2">
|
||||
<span
|
||||
class="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-indigo-600 to-indigo-400"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="white"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><path d="M2 12c1.5 0 1.5-2 3-2s1.5 2 3 2 1.5-2 3-2 1.5 2 3 2 1.5-2 3-2 1.5 2 3 2" /><path
|
||||
d="m2 17 1.5-2M22 12c-1.5 0-1.5-2-3-2M22 17l-1.5-2"
|
||||
/></svg
|
||||
>
|
||||
</span>
|
||||
<span class="text-base font-semibold tracking-tight text-slate-900">Radiant Wave</span>
|
||||
</a>
|
||||
|
||||
{#if $session.data?.user}
|
||||
<nav aria-label="Primary" class="hidden items-center gap-1 md:flex">
|
||||
<a
|
||||
href={resolve('/')}
|
||||
class={`rounded-md px-3 py-2 text-sm font-medium transition ${activeClass('/')}`}
|
||||
>
|
||||
Home
|
||||
</a>
|
||||
<a
|
||||
href={resolve('/dashboard')}
|
||||
class={`rounded-md px-3 py-2 text-sm font-medium transition ${activeClass('/dashboard')}`}
|
||||
>
|
||||
Dashboard
|
||||
</a>
|
||||
<a
|
||||
href={resolve('/account')}
|
||||
class={`rounded-md px-3 py-2 text-sm font-medium transition ${activeClass('/account')}`}
|
||||
>
|
||||
Account
|
||||
</a>
|
||||
<a
|
||||
href={resolve('/license-keys')}
|
||||
class={`rounded-md px-3 py-2 text-sm font-medium transition ${activeClass('/license-keys')}`}
|
||||
>
|
||||
License Keys
|
||||
</a>
|
||||
|
||||
{#if isAdmin}
|
||||
<div class="relative">
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={adminOpen}
|
||||
onclick={() => (adminOpen = !adminOpen)}
|
||||
class={`flex items-center gap-1 rounded-md px-3 py-2 text-sm font-medium transition ${
|
||||
isActive('/admin')
|
||||
? 'bg-indigo-50 text-indigo-700'
|
||||
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900'
|
||||
}`}
|
||||
>
|
||||
Admin
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="transition-transform"
|
||||
class:rotate-180={adminOpen}><path d="m6 9 6 6 6-6" /></svg
|
||||
>
|
||||
</button>
|
||||
|
||||
{#if adminOpen}
|
||||
<div
|
||||
class="absolute top-full left-0 mt-1 min-w-56 rounded-lg border border-slate-200 bg-white p-1 shadow-lg"
|
||||
>
|
||||
{#each adminModels as model (model.slug)}
|
||||
<a
|
||||
href="/admin/{model.slug}"
|
||||
onclick={() => (adminOpen = false)}
|
||||
class={`block rounded-md px-3 py-2 text-sm transition ${
|
||||
isActive('/admin/' + model.slug)
|
||||
? 'bg-indigo-50 text-indigo-700'
|
||||
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900'
|
||||
}`}
|
||||
>
|
||||
{model.displayName}
|
||||
</a>
|
||||
{/each}
|
||||
<a
|
||||
href={resolve('/admin/api-keys')}
|
||||
onclick={() => (adminOpen = false)}
|
||||
class={`block rounded-md px-3 py-2 text-sm transition ${
|
||||
isActive('/admin/api-keys')
|
||||
? 'bg-indigo-50 text-indigo-700'
|
||||
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900'
|
||||
}`}
|
||||
>
|
||||
API Keys
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<span class="mx-2 h-5 w-px bg-slate-200" aria-hidden="true"></span>
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleSignOut}
|
||||
class="rounded-md px-3 py-2 text-sm font-medium text-slate-500 transition hover:bg-slate-100 hover:text-slate-900"
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
</nav>
|
||||
{:else}
|
||||
<span class="dark:text-gray-300"
|
||||
><a href="/login" class="dark:text-gray-300 dark:hover:text-gray-100">Log In</a> |
|
||||
<a href="/register" class="dark:text-gray-300 dark:hover:text-gray-100">Register</a></span
|
||||
>
|
||||
<nav aria-label="Primary" class="hidden items-center gap-1 md:flex">
|
||||
<a
|
||||
href={resolve('/')}
|
||||
class={`rounded-md px-3 py-2 text-sm font-medium transition ${activeClass('/')}`}
|
||||
>
|
||||
Home
|
||||
</a>
|
||||
<a
|
||||
href={resolve('/login')}
|
||||
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-indigo-700"
|
||||
>
|
||||
Sign In
|
||||
</a>
|
||||
</nav>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Toggle navigation menu"
|
||||
aria-expanded={menuOpen}
|
||||
onclick={() => (menuOpen = !menuOpen)}
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg text-slate-700 hover:bg-slate-100 md:hidden"
|
||||
>
|
||||
{#if menuOpen}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg
|
||||
>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><line x1="4" x2="20" y1="6" y2="6" /><line x1="4" x2="20" y1="12" y2="12" /><line
|
||||
x1="4"
|
||||
x2="20"
|
||||
y1="18"
|
||||
y2="18"
|
||||
/></svg
|
||||
>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="flex min-h-0 flex-1 px-8 dark:bg-gray-950">
|
||||
{@render children()}
|
||||
</header>
|
||||
|
||||
{#if menuOpen}
|
||||
<div
|
||||
class="fixed inset-0 top-14 z-50 overflow-y-auto border-t border-slate-200 bg-white shadow-lg md:hidden"
|
||||
>
|
||||
<nav class="mx-auto max-w-6xl px-4 py-4">
|
||||
{#if $session.data?.user}
|
||||
<div class="flex flex-col gap-1">
|
||||
<a
|
||||
href={resolve('/')}
|
||||
onclick={closeMenu}
|
||||
class={`rounded-lg px-4 py-3 text-base font-medium transition ${activeClass('/')}`}
|
||||
>
|
||||
Home
|
||||
</a>
|
||||
<a
|
||||
href={resolve('/dashboard')}
|
||||
onclick={closeMenu}
|
||||
class={`rounded-lg px-4 py-3 text-base font-medium transition ${activeClass('/dashboard')}`}
|
||||
>
|
||||
Dashboard
|
||||
</a>
|
||||
<a
|
||||
href={resolve('/account')}
|
||||
onclick={closeMenu}
|
||||
class={`rounded-lg px-4 py-3 text-base font-medium transition ${activeClass('/account')}`}
|
||||
>
|
||||
Account
|
||||
</a>
|
||||
<a
|
||||
href={resolve('/license-keys')}
|
||||
onclick={closeMenu}
|
||||
class={`rounded-lg px-4 py-3 text-base font-medium transition ${activeClass('/license-keys')}`}
|
||||
>
|
||||
License Keys
|
||||
</a>
|
||||
|
||||
{#if isAdmin}
|
||||
<div class="mt-2 border-t border-slate-200 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={adminOpen}
|
||||
onclick={() => (adminOpen = !adminOpen)}
|
||||
class={`flex w-full items-center justify-between rounded-lg px-4 py-3 text-base font-semibold transition ${
|
||||
isActive('/admin')
|
||||
? 'bg-indigo-50 text-indigo-700'
|
||||
: 'text-slate-700 hover:bg-indigo-50'
|
||||
}`}
|
||||
>
|
||||
<span>Admin</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="transition-transform"
|
||||
class:rotate-180={adminOpen}><path d="m6 9 6 6 6-6" /></svg
|
||||
>
|
||||
</button>
|
||||
|
||||
{#if adminOpen}
|
||||
<div class="mt-1 flex flex-col gap-1">
|
||||
{#each adminModels as model (model.slug)}
|
||||
<a
|
||||
href="/admin/{model.slug}"
|
||||
onclick={closeMenu}
|
||||
class={`rounded-lg px-4 py-3 text-base transition ${activeClass('/admin/' + model.slug)}`}
|
||||
>
|
||||
{model.displayName}
|
||||
</a>
|
||||
{/each}
|
||||
<a
|
||||
href={resolve('/admin/api-keys')}
|
||||
onclick={closeMenu}
|
||||
class={`rounded-lg px-4 py-3 text-base transition ${activeClass('/admin/api-keys')}`}
|
||||
>
|
||||
API Keys
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleSignOut}
|
||||
class="mt-2 rounded-lg border-t border-slate-200 px-4 py-3 text-base font-medium text-slate-500 hover:bg-slate-100"
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-1">
|
||||
<a
|
||||
href={resolve('/')}
|
||||
onclick={closeMenu}
|
||||
class={`rounded-lg px-4 py-3 text-base font-medium transition ${activeClass('/')}`}
|
||||
>
|
||||
Home
|
||||
</a>
|
||||
<a
|
||||
href={resolve('/login')}
|
||||
onclick={closeMenu}
|
||||
class="mt-1 rounded-lg bg-indigo-600 px-4 py-3 text-center text-base font-medium text-white hover:bg-indigo-700"
|
||||
>
|
||||
Sign In
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</nav>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex min-h-[calc(100dvh-3.5rem)] flex-col bg-slate-50 text-slate-900">
|
||||
<main class="flex-1">
|
||||
{@render children()}
|
||||
</main>
|
||||
<footer class="sticky bottom-0 z-10 border-t border-slate-200 bg-white">
|
||||
<div
|
||||
class="mx-auto flex max-w-6xl flex-col items-center justify-between gap-2 px-4 py-6 text-sm text-slate-500 sm:flex-row"
|
||||
>
|
||||
<p>© {new Date().getFullYear()} Radiant Wave Technologies. All rights reserved.</p>
|
||||
{#if $session.data?.user}
|
||||
<a href={resolve('/account')} class="transition hover:text-slate-900">
|
||||
Manage your account
|
||||
</a>
|
||||
{:else}
|
||||
<a href={resolve('/login')} class="transition hover:text-slate-900">Sign in</a>
|
||||
{/if}
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
+41
-3
@@ -1,3 +1,41 @@
|
||||
<div class="flex flex-1 flex-col items-center justify-center">
|
||||
<h1 class="dark:text-gray-100">Hello</h1>
|
||||
</div>
|
||||
<script lang="ts">
|
||||
import { authClient } from '$lib/auth-client';
|
||||
import { resolve } from '$app/paths';
|
||||
|
||||
const session = authClient.useSession();
|
||||
const signedIn = $derived(!!$session.data?.user);
|
||||
</script>
|
||||
|
||||
<section class="py-16 sm:py-24">
|
||||
<div class="mx-auto w-full max-w-6xl px-4">
|
||||
<div class="mx-auto flex max-w-3xl flex-col items-center text-center">
|
||||
<h1 class="mt-6 text-4xl font-bold tracking-tight text-slate-900 sm:text-5xl">
|
||||
Welcome to the Radiant Wave Portal
|
||||
</h1>
|
||||
|
||||
<div class="mt-8 flex flex-col items-center gap-3 sm:flex-row">
|
||||
{#if signedIn}
|
||||
<a
|
||||
href={resolve('/dashboard')}
|
||||
class="inline-flex h-12 items-center justify-center rounded-lg bg-indigo-600 px-8 text-base font-medium text-white shadow-sm transition hover:bg-indigo-700"
|
||||
>
|
||||
Go to Dashboard
|
||||
</a>
|
||||
{:else}
|
||||
<a
|
||||
href={resolve('/login')}
|
||||
class="inline-flex h-12 w-full items-center justify-center rounded-lg bg-indigo-600 px-8 text-base font-medium text-white shadow-sm transition hover:bg-indigo-700 sm:w-auto"
|
||||
>
|
||||
Get Started
|
||||
</a>
|
||||
<a
|
||||
href={resolve('/register')}
|
||||
class="inline-flex h-12 w-full items-center justify-center rounded-lg border border-slate-300 bg-white px-8 text-base font-medium text-slate-700 transition hover:bg-slate-50 sm:w-auto"
|
||||
>
|
||||
Create Account
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -12,21 +12,11 @@ function generateLicenseKey(): string {
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
const body = await request.json();
|
||||
const { email, name, subscription_level } = body;
|
||||
const { email, name } = body;
|
||||
|
||||
if (!email || typeof email !== 'string') {
|
||||
error(400, 'email is required');
|
||||
}
|
||||
if (!subscription_level || typeof subscription_level !== 'string') {
|
||||
error(400, 'subscription_level is required');
|
||||
}
|
||||
|
||||
const level = await prisma.subscriptionLevel.findFirst({
|
||||
where: { name: subscription_level }
|
||||
});
|
||||
if (!level) {
|
||||
error(400, `subscription_level "${subscription_level}" not found`);
|
||||
}
|
||||
|
||||
let user = await prisma.user.findUnique({ where: { email } });
|
||||
|
||||
@@ -83,7 +73,6 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||
data: {
|
||||
key,
|
||||
active: true,
|
||||
level: subscription_level,
|
||||
userId: user.id
|
||||
}
|
||||
});
|
||||
@@ -91,7 +80,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||
try {
|
||||
await sendMail({
|
||||
to: email,
|
||||
subject: 'Your Rhemawave License Key',
|
||||
subject: 'Your Radiant Wave License Key',
|
||||
templateId: 'license-key-delivery',
|
||||
data: {
|
||||
firstName: name ?? 'Valued Customer',
|
||||
|
||||
@@ -39,8 +39,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
key: storedKey.key,
|
||||
email_address: email,
|
||||
subscription_level: storedKey.level
|
||||
email_address: email
|
||||
}),
|
||||
{ headers: { 'content-type': 'application/json' } }
|
||||
);
|
||||
|
||||
@@ -61,7 +61,7 @@ export const POST: RequestHandler = async ({ request, url }) => {
|
||||
throw error(400, 'invalid JSON body');
|
||||
}
|
||||
|
||||
const { email, name, subscription_level } = body as Record<string, unknown>;
|
||||
const { email, name } = body as Record<string, unknown>;
|
||||
|
||||
if (typeof email !== 'string' || !email.trim()) {
|
||||
error(400, 'email is required');
|
||||
@@ -71,10 +71,6 @@ export const POST: RequestHandler = async ({ request, url }) => {
|
||||
error(400, 'email is required');
|
||||
}
|
||||
|
||||
if (typeof subscription_level !== 'string' || !subscription_level.trim()) {
|
||||
error(400, 'subscription_level is required');
|
||||
}
|
||||
|
||||
const emailLimit = rateLimit({
|
||||
key: `onboarding:email:${cleanEmail}`,
|
||||
limit: 3,
|
||||
@@ -84,12 +80,6 @@ export const POST: RequestHandler = async ({ request, url }) => {
|
||||
return rateLimited(emailLimit.retryAfterMs);
|
||||
}
|
||||
|
||||
const levelName = subscription_level.trim();
|
||||
const level = await prisma.subscriptionLevel.findFirst({ where: { name: levelName } });
|
||||
if (!level) {
|
||||
error(400, `subscription_level "${levelName}" not found`);
|
||||
}
|
||||
|
||||
const displayName =
|
||||
typeof name === 'string' && name.trim() ? name.trim().slice(0, 120) : cleanEmail.split('@')[0];
|
||||
|
||||
@@ -148,7 +138,6 @@ export const POST: RequestHandler = async ({ request, url }) => {
|
||||
data: {
|
||||
key,
|
||||
active: true,
|
||||
level: levelName,
|
||||
userId: user.id
|
||||
}
|
||||
});
|
||||
@@ -156,7 +145,7 @@ export const POST: RequestHandler = async ({ request, url }) => {
|
||||
try {
|
||||
await sendMail({
|
||||
to: cleanEmail,
|
||||
subject: 'Your Rhemawave License Key',
|
||||
subject: 'Your Radiant Wave License Key',
|
||||
templateId: 'license-key-delivery',
|
||||
data: {
|
||||
firstName: displayName,
|
||||
|
||||
@@ -25,7 +25,6 @@ vi.mock('$lib/server/resend', () => ({
|
||||
|
||||
vi.mock('$lib/db', () => ({
|
||||
prisma: {
|
||||
subscriptionLevel: { findFirst: vi.fn() },
|
||||
user: { findUnique: vi.fn() },
|
||||
verification: { findFirst: vi.fn() },
|
||||
licenseKey: { create: vi.fn() }
|
||||
@@ -40,7 +39,6 @@ type POSTEvent = Parameters<typeof POST>[0];
|
||||
const SECRET = 'test-onboarding-secret';
|
||||
const PATH = '/api/v2/automations/create/account';
|
||||
|
||||
const mockLevelFindFirst = prisma.subscriptionLevel.findFirst as ReturnType<typeof vi.fn>;
|
||||
const mockUserFindUnique = prisma.user.findUnique as ReturnType<typeof vi.fn>;
|
||||
const mockVerification = prisma.verification.findFirst as ReturnType<typeof vi.fn>;
|
||||
const mockLicenseCreate = prisma.licenseKey.create as ReturnType<typeof vi.fn>;
|
||||
@@ -88,13 +86,12 @@ describe('POST /api/v2/automations/create/account', () => {
|
||||
});
|
||||
|
||||
it('creates a license key for an existing user without a new signup', async () => {
|
||||
mockLevelFindFirst.mockResolvedValue({ id: 1, name: 'Basic', description: 'General only' });
|
||||
mockUserFindUnique.mockResolvedValue({ id: 'user-1', email: 'user@example.com' });
|
||||
mockLicenseCreate.mockResolvedValue({ id: 'lic-1' });
|
||||
sendMailMock.mockResolvedValue({});
|
||||
|
||||
const res = await POST({
|
||||
request: signedOnboardingRequest({ email: 'user@example.com', subscription_level: 'Basic' }),
|
||||
request: signedOnboardingRequest({ email: 'user@example.com' }),
|
||||
url: new URL(`http://localhost${PATH}`)
|
||||
} as POSTEvent);
|
||||
|
||||
@@ -105,12 +102,15 @@ describe('POST /api/v2/automations/create/account', () => {
|
||||
user_id: 'user-1'
|
||||
});
|
||||
expect(authApi.signUpEmail).not.toHaveBeenCalled();
|
||||
expect(mockLicenseCreate).toHaveBeenCalledTimes(1);
|
||||
expect(mockLicenseCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({ active: true })
|
||||
})
|
||||
);
|
||||
expect(sendMailMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('signs up a new user and sends registration + license emails', async () => {
|
||||
mockLevelFindFirst.mockResolvedValue({ id: 1, name: 'Basic', description: 'General only' });
|
||||
mockUserFindUnique
|
||||
.mockResolvedValueOnce(null)
|
||||
.mockResolvedValueOnce({ id: 'user-2', email: 'new@example.com' });
|
||||
@@ -120,7 +120,7 @@ describe('POST /api/v2/automations/create/account', () => {
|
||||
authApi.signUpEmail.mockResolvedValue({});
|
||||
|
||||
const res = await POST({
|
||||
request: signedOnboardingRequest({ email: 'new@example.com', subscription_level: 'Elite' }),
|
||||
request: signedOnboardingRequest({ email: 'new@example.com' }),
|
||||
url: new URL(`http://localhost${PATH}`)
|
||||
} as POSTEvent);
|
||||
|
||||
@@ -142,7 +142,7 @@ describe('POST /api/v2/automations/create/account', () => {
|
||||
const req = new Request(`http://localhost${PATH}`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ email: 'user@example.com', subscription_level: 'Basic' })
|
||||
body: JSON.stringify({ email: 'user@example.com' })
|
||||
});
|
||||
|
||||
const res = await POST({ request: req, url: new URL(`http://localhost${PATH}`) } as POSTEvent);
|
||||
@@ -152,10 +152,7 @@ describe('POST /api/v2/automations/create/account', () => {
|
||||
|
||||
it('rejects a request signed with the wrong secret', async () => {
|
||||
const res = await POST({
|
||||
request: signedOnboardingRequest(
|
||||
{ email: 'user@example.com', subscription_level: 'Basic' },
|
||||
'wrong-secret'
|
||||
),
|
||||
request: signedOnboardingRequest({ email: 'user@example.com' }, 'wrong-secret'),
|
||||
url: new URL(`http://localhost${PATH}`)
|
||||
} as POSTEvent);
|
||||
expect(res.status).toBe(401);
|
||||
@@ -165,7 +162,7 @@ describe('POST /api/v2/automations/create/account', () => {
|
||||
it('rejects a missing email', async () => {
|
||||
await expectHttpError(
|
||||
POST({
|
||||
request: signedOnboardingRequest({ subscription_level: 'Basic' }),
|
||||
request: signedOnboardingRequest({}),
|
||||
url: new URL(`http://localhost${PATH}`)
|
||||
} as POSTEvent),
|
||||
400,
|
||||
@@ -176,7 +173,6 @@ describe('POST /api/v2/automations/create/account', () => {
|
||||
it('rejects an oversized body with 413', async () => {
|
||||
const rawBody = JSON.stringify({
|
||||
email: 'user@example.com',
|
||||
subscription_level: 'Basic',
|
||||
name: 'x'.repeat(64 * 1024 + 1)
|
||||
});
|
||||
expect(rawBody.length).toBeGreaterThan(64 * 1024);
|
||||
@@ -194,40 +190,21 @@ describe('POST /api/v2/automations/create/account', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects an unknown subscription level', async () => {
|
||||
mockLevelFindFirst.mockResolvedValue(null);
|
||||
await expectHttpError(
|
||||
POST({
|
||||
request: signedOnboardingRequest({
|
||||
email: 'user@example.com',
|
||||
subscription_level: 'Titanium'
|
||||
}),
|
||||
url: new URL(`http://localhost${PATH}`)
|
||||
} as POSTEvent),
|
||||
400,
|
||||
'subscription_level "Titanium" not found'
|
||||
);
|
||||
});
|
||||
|
||||
it('rate limits repeated requests for the same email', async () => {
|
||||
mockLevelFindFirst.mockResolvedValue({ id: 1, name: 'Basic', description: 'General only' });
|
||||
mockUserFindUnique.mockResolvedValue({ id: 'user-1', email: 'user@example.com' });
|
||||
mockLicenseCreate.mockResolvedValue({ id: 'lic-1' });
|
||||
sendMailMock.mockResolvedValue({});
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const res = await POST({
|
||||
request: signedOnboardingRequest({
|
||||
email: 'user@example.com',
|
||||
subscription_level: 'Basic'
|
||||
}),
|
||||
request: signedOnboardingRequest({ email: 'user@example.com' }),
|
||||
url: new URL(`http://localhost${PATH}`)
|
||||
} as POSTEvent);
|
||||
expect(res.status).toBe(200);
|
||||
}
|
||||
|
||||
const res = await POST({
|
||||
request: signedOnboardingRequest({ email: 'user@example.com', subscription_level: 'Basic' }),
|
||||
request: signedOnboardingRequest({ email: 'user@example.com' }),
|
||||
url: new URL(`http://localhost${PATH}`)
|
||||
} as POSTEvent);
|
||||
expect(res.status).toBe(429);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { RequestHandler } from './$types';
|
||||
import { prisma } from '$lib/db';
|
||||
import { jsonOk } from '$lib/server/v2-helpers';
|
||||
import { serializeGroup } from '$lib/server/v2-serializers';
|
||||
|
||||
export const GET: RequestHandler = async () => {
|
||||
const groups = await prisma.proclamationGroup.findMany({
|
||||
orderBy: { sortOrder: 'asc' }
|
||||
});
|
||||
return jsonOk(groups.map(serializeGroup));
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
vi.mock('$lib/db', () => ({
|
||||
prisma: {
|
||||
proclamationGroup: { findMany: vi.fn() }
|
||||
}
|
||||
}));
|
||||
|
||||
import { prisma } from '$lib/db';
|
||||
import { GET } from './+server';
|
||||
|
||||
type GETEvent = Parameters<typeof GET>[0];
|
||||
|
||||
const mockFindMany = prisma.proclamationGroup.findMany as ReturnType<typeof vi.fn>;
|
||||
|
||||
describe('GET /api/v2/groups', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns groups ordered by sortOrder', async () => {
|
||||
mockFindMany.mockResolvedValue([
|
||||
{ id: 1, name: 'General', sortOrder: 0 },
|
||||
{ id: 2, name: 'Specific', sortOrder: 1 },
|
||||
{ id: 3, name: 'Precise', sortOrder: 2 }
|
||||
]);
|
||||
|
||||
const res = await GET({} as GETEvent);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual([
|
||||
{ id: 1, name: 'General', sortOrder: 0 },
|
||||
{ id: 2, name: 'Specific', sortOrder: 1 },
|
||||
{ id: 3, name: 'Precise', sortOrder: 2 }
|
||||
]);
|
||||
expect(mockFindMany).toHaveBeenCalledWith({ orderBy: { sortOrder: 'asc' } });
|
||||
});
|
||||
|
||||
it('returns an empty list when no groups exist', async () => {
|
||||
mockFindMany.mockResolvedValue([]);
|
||||
|
||||
const res = await GET({} as GETEvent);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -50,16 +50,10 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||
throw error(400, 'key does not belong to specified user');
|
||||
}
|
||||
|
||||
const level = await prisma.subscriptionLevel.findFirst({
|
||||
where: { name: storedKey.level }
|
||||
});
|
||||
|
||||
return jsonOk(
|
||||
serializeLicense({
|
||||
key: storedKey.key,
|
||||
emailAddress: email,
|
||||
levelName: storedKey.level,
|
||||
level
|
||||
emailAddress: email
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,8 +5,7 @@ import { _clearRateLimits } from '$lib/server/v2-rate-limit';
|
||||
vi.mock('$lib/db', () => ({
|
||||
prisma: {
|
||||
licenseKey: { findFirst: vi.fn() },
|
||||
user: { findUnique: vi.fn() },
|
||||
subscriptionLevel: { findFirst: vi.fn() }
|
||||
user: { findUnique: vi.fn() }
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -17,7 +16,6 @@ type POSTEvent = Parameters<typeof POST>[0];
|
||||
|
||||
const mockKeyFindFirst = prisma.licenseKey.findFirst as ReturnType<typeof vi.fn>;
|
||||
const mockUserFindUnique = prisma.user.findUnique as ReturnType<typeof vi.fn>;
|
||||
const mockLevelFindFirst = prisma.subscriptionLevel.findFirst as ReturnType<typeof vi.fn>;
|
||||
|
||||
function licenseRequest(body: unknown): Request {
|
||||
return new Request('http://localhost/api/v2/license', {
|
||||
@@ -44,7 +42,6 @@ const storedKey = {
|
||||
id: 'key-1',
|
||||
key: 'ABCD-EFGH-IJKL-MNOP',
|
||||
active: true,
|
||||
level: 'Advanced',
|
||||
userId: 'user-1'
|
||||
};
|
||||
|
||||
@@ -54,42 +51,21 @@ describe('POST /api/v2/license', () => {
|
||||
_clearRateLimits();
|
||||
});
|
||||
|
||||
it('returns the subscription_level object for a valid key', async () => {
|
||||
it('returns key and email_address for a valid key', async () => {
|
||||
mockKeyFindFirst.mockResolvedValue(storedKey);
|
||||
mockUserFindUnique.mockResolvedValue({ id: 'user-1', email: 'user@example.com' });
|
||||
mockLevelFindFirst.mockResolvedValue({
|
||||
id: 2,
|
||||
name: 'Advanced',
|
||||
description: 'General + Specific'
|
||||
});
|
||||
|
||||
const res = await POST({
|
||||
request: licenseRequest({ license_key: storedKey.key, email_address: 'user@example.com' })
|
||||
} as POSTEvent);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual({
|
||||
const body = (await res.json()) as Record<string, unknown>;
|
||||
expect(body).toEqual({
|
||||
key: storedKey.key,
|
||||
email_address: 'user@example.com',
|
||||
subscription_level: { id: 2, name: 'Advanced', description: 'General + Specific' }
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to a structurally consistent subscription_level for an unknown level name', async () => {
|
||||
mockKeyFindFirst.mockResolvedValue({ ...storedKey, level: 'SomeCustomLevel' });
|
||||
mockUserFindUnique.mockResolvedValue({ id: 'user-1', email: 'user@example.com' });
|
||||
mockLevelFindFirst.mockResolvedValue(null);
|
||||
|
||||
const res = await POST({
|
||||
request: licenseRequest({ license_key: storedKey.key, email_address: 'user@example.com' })
|
||||
} as POSTEvent);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual({
|
||||
key: storedKey.key,
|
||||
email_address: 'user@example.com',
|
||||
subscription_level: { id: null, name: 'SomeCustomLevel', description: null }
|
||||
email_address: 'user@example.com'
|
||||
});
|
||||
expect(Object.keys(body)).not.toContain('subscription_level');
|
||||
});
|
||||
|
||||
it('rejects a missing license_key', async () => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user