Build user pages

This commit is contained in:
NeilClack
2026-02-25 13:19:26 -06:00
parent 12190d01dc
commit cc51f40591
53 changed files with 5204 additions and 4817 deletions
@@ -1,260 +0,0 @@
<script lang="ts">
import TipTap from '$lib/components/tiptap/TipTap.svelte';
type Category = {
id: string;
name: string;
parentId?: number | null;
children?: Category[];
};
type Frequency = {
id: string;
name: string;
};
type Affirmation = {
id?: string;
name?: string;
content?: string;
price?: number;
categoryId?: string;
solfeggioFrequencyId?: string;
category?: { parentId?: number | null; parent?: { name: string } };
};
type FormData = {
id?: string;
name?: string;
content?: string;
price?: number;
categoryId?: string;
categoryName?: string;
subCategoryName?: string;
solfeggioFrequencyId?: string;
};
type Props = {
affirmation?: Affirmation;
categories: Category[];
frequencies: Frequency[];
form?: FormData;
action: string;
submitLabel?: string;
};
let {
affirmation = {},
categories,
frequencies,
form,
action,
submitLabel = 'Save Affirmation'
}: Props = $props();
const topLevelCategories = $derived(categories.filter((c) => !c.parentId));
// Determine initial parent category name from form data or existing affirmation
function getInitialCategoryName(): string {
if (form?.categoryName) return form.categoryName;
if (affirmation.category?.parent) return affirmation.category.parent.name;
if (affirmation.categoryId) {
const cat = categories.find((c) => c.id === affirmation.categoryId);
if (cat?.parentId) {
const parent = categories.find((c) => String(c.id) === String(cat.parentId));
return parent?.name ?? '';
}
return cat?.name ?? '';
}
return '';
}
function getInitialSubCategoryName(): string {
if (form?.subCategoryName) return form.subCategoryName;
if (affirmation.categoryId) {
const cat = categories.find((c) => c.id === affirmation.categoryId);
if (cat?.parentId) return cat.name;
}
return '';
}
let categoryInput = $state(getInitialCategoryName());
let showNewCategory = $state(false);
let newCategoryName = $state('');
let subCategoryInput = $state(getInitialSubCategoryName());
let showNewSubCategory = $state(false);
let newSubCategoryName = $state('');
const selectedParent = $derived(topLevelCategories.find((c) => c.name === categoryInput));
const subCategories = $derived(selectedParent?.children ?? []);
function handleCategoryChange(e: Event) {
const value = (e.target as HTMLSelectElement).value;
if (value === '__new__') {
showNewCategory = true;
categoryInput = '';
} else {
showNewCategory = false;
newCategoryName = '';
categoryInput = value;
}
subCategoryInput = '';
showNewSubCategory = false;
newSubCategoryName = '';
}
function handleSubCategoryChange(e: Event) {
const value = (e.target as HTMLSelectElement).value;
if (value === '__new__') {
showNewSubCategory = true;
subCategoryInput = '';
} else {
showNewSubCategory = false;
newSubCategoryName = '';
subCategoryInput = value;
}
}
const effectiveCategoryName = $derived(showNewCategory ? newCategoryName : categoryInput);
const effectiveSubCategoryName = $derived(
showNewSubCategory ? newSubCategoryName : subCategoryInput
);
</script>
<form method="POST" {action} class="space-y-5">
{#if affirmation.id || form?.id}
<input type="hidden" name="id" value={form?.id ?? affirmation.id} />
{/if}
<input type="hidden" name="categoryName" value={effectiveCategoryName} />
<input type="hidden" name="subCategoryName" value={effectiveSubCategoryName} />
<div>
<label for="name" class="mb-1 block text-sm font-medium text-gray-700">Name</label>
<input
type="text"
id="name"
name="name"
value={form?.name ?? affirmation.name ?? ''}
required
class="w-full rounded-lg border border-gray-300 px-3 py-2 outline-none transition-shadow focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-gray-700" for="content">Content</label>
<TipTap initialContent={form?.content ?? affirmation.content ?? ''} name="content" />
</div>
<div>
<label for="solfeggioFrequencyId" class="mb-1 block text-sm font-medium text-gray-700">
Solfeggio Frequency
</label>
<select
id="solfeggioFrequencyId"
name="solfeggioFrequencyId"
required
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 outline-none transition-shadow focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
>
<option value="" disabled selected={!affirmation.solfeggioFrequencyId && !form?.solfeggioFrequencyId}>
Select a frequency
</option>
{#each frequencies as frequency}
<option
value={frequency.id}
selected={frequency.id === (form?.solfeggioFrequencyId ?? affirmation.solfeggioFrequencyId)}
>
{frequency.name}
</option>
{/each}
</select>
</div>
<div>
<label for="categorySelect" class="mb-1 block text-sm font-medium text-gray-700">
Category
</label>
<select
id="categorySelect"
onchange={handleCategoryChange}
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 outline-none transition-shadow focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
>
<option value="" disabled selected={!categoryInput && !showNewCategory}>
Select a category
</option>
{#each topLevelCategories as category}
<option value={category.name} selected={category.name === categoryInput}>
{category.name}
</option>
{/each}
<option value="__new__" selected={showNewCategory}>New...</option>
</select>
{#if showNewCategory}
<input
type="text"
placeholder="New category name"
bind:value={newCategoryName}
required
class="mt-2 w-full rounded-lg border border-gray-300 px-3 py-2 outline-none transition-shadow focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
/>
{/if}
</div>
{#if categoryInput || showNewCategory}
<div>
<label for="subCategorySelect" class="mb-1 block text-sm font-medium text-gray-700">
Sub-category
</label>
<select
id="subCategorySelect"
onchange={handleSubCategoryChange}
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 outline-none transition-shadow focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
>
<option value="" selected={!subCategoryInput && !showNewSubCategory}>None</option>
{#each subCategories as sub}
<option value={sub.name} selected={sub.name === subCategoryInput}>
{sub.name}
</option>
{/each}
<option value="__new__" selected={showNewSubCategory}>New...</option>
</select>
{#if showNewSubCategory}
<input
type="text"
placeholder="New sub-category name"
bind:value={newSubCategoryName}
class="mt-2 w-full rounded-lg border border-gray-300 px-3 py-2 outline-none transition-shadow focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
/>
{/if}
</div>
{/if}
<div>
<label for="price" class="mb-1 block text-sm font-medium text-gray-700">Price</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">$</span>
<input
type="number"
id="price"
name="price"
value={form?.price ?? affirmation.price ?? ''}
defaultValue={0.00}
min="0"
step="0.01"
placeholder="0.00"
required
class="w-full rounded-lg border border-gray-300 py-2 pr-3 pl-7 outline-none transition-shadow focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
<button
type="submit"
class="rounded-lg bg-blue-600 px-4 py-2.5 font-medium text-white transition-colors hover:bg-blue-700 focus:ring-4 focus:ring-blue-200"
>
{submitLabel}
</button>
</form>
@@ -1,62 +0,0 @@
<script lang="ts">
type Frequency = {
id?: number;
name?: string;
value?: number;
};
type FormData = {
id?: string;
name?: string;
value?: string;
};
type Props = {
frequency?: Frequency;
form?: FormData;
action: string;
submitLabel?: string;
};
let { frequency = {}, form, action, submitLabel = 'Save Frequency' }: Props = $props();
</script>
<form method="POST" {action} class="space-y-5">
{#if frequency.id || form?.id}
<input type="hidden" name="id" value={form?.id ?? frequency.id} />
{/if}
<div>
<label for="name" class="mb-1 block text-sm font-medium text-gray-700">Name</label>
<input
type="text"
id="name"
name="name"
value={form?.name ?? frequency.name ?? ''}
required
class="w-full rounded-lg border border-gray-300 px-3 py-2 outline-none transition-shadow focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label for="value" class="mb-1 block text-sm font-medium text-gray-700">Frequency (Hz)</label>
<input
type="number"
id="value"
name="value"
value={form?.value ?? frequency.value ?? ''}
min="0"
step="0.01"
placeholder="e.g. 396"
required
class="w-full rounded-lg border border-gray-300 px-3 py-2 outline-none transition-shadow focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
/>
</div>
<button
type="submit"
class="rounded-lg bg-blue-600 px-4 py-2.5 font-medium text-white transition-colors hover:bg-blue-700 focus:ring-4 focus:ring-blue-200"
>
{submitLabel}
</button>
</form>
@@ -1,242 +0,0 @@
<script lang="ts">
type Affirmation = {
id: string;
name: string;
content: string;
price: number;
solfeggioFrequency?: {
name: string;
value: number;
};
category?: {
name: string;
parent?: { name: string } | null;
};
};
function categoryDisplayName(category?: Affirmation['category']): string {
if (!category) return '';
if (category.parent) return `${category.parent.name} > ${category.name}`;
return category.name;
}
type SortKey = 'name' | 'category' | 'frequency' | 'price';
type SortDirection = 'asc' | 'desc';
type Props = {
affirmations: Affirmation[];
};
let { affirmations }: Props = $props();
let search = $state('');
let categoryFilter = $state('');
let frequencyFilter = $state('');
let sortKey = $state<SortKey>('name');
let sortDirection = $state<SortDirection>('asc');
const categories = $derived([...new Set(affirmations.map((a) => categoryDisplayName(a.category)).filter(Boolean))].sort());
const frequencies = $derived([...new Set(affirmations.map((a) => a.solfeggioFrequency?.name).filter(Boolean))].sort());
const filteredAndSorted = $derived(() => {
let result = affirmations;
// Search filter
if (search) {
const searchLower = search.toLowerCase();
result = result.filter(
(a) =>
a.name.toLowerCase().includes(searchLower) ||
a.content.toLowerCase().includes(searchLower) ||
categoryDisplayName(a.category).toLowerCase().includes(searchLower) ||
a.solfeggioFrequency?.name.toLowerCase().includes(searchLower)
);
}
// Category filter
if (categoryFilter) {
result = result.filter((a) => categoryDisplayName(a.category) === categoryFilter);
}
// Frequency filter
if (frequencyFilter) {
result = result.filter((a) => a.solfeggioFrequency?.name === frequencyFilter);
}
// Sort
result = [...result].sort((a, b) => {
let aVal: string | number;
let bVal: string | number;
switch (sortKey) {
case 'name':
aVal = a.name.toLowerCase();
bVal = b.name.toLowerCase();
break;
case 'category':
aVal = categoryDisplayName(a.category).toLowerCase();
bVal = categoryDisplayName(b.category).toLowerCase();
break;
case 'frequency':
aVal = a.solfeggioFrequency?.value ?? 0;
bVal = b.solfeggioFrequency?.value ?? 0;
break;
case 'price':
aVal = a.price;
bVal = b.price;
break;
}
if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1;
if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1;
return 0;
});
return result;
});
function toggleSort(key: SortKey) {
if (sortKey === key) {
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
} else {
sortKey = key;
sortDirection = 'asc';
}
}
function getSortIndicator(key: SortKey) {
if (sortKey !== key) return '↕';
return sortDirection === 'asc' ? '↑' : '↓';
}
function clearFilters() {
search = '';
categoryFilter = '';
frequencyFilter = '';
}
const hasActiveFilters = $derived(search || categoryFilter || frequencyFilter);
</script>
<div class="flex flex-col gap-4">
<div class="flex flex-wrap items-center gap-3">
<input
type="text"
placeholder="Search affirmations..."
bind:value={search}
class="rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none transition-shadow focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
/>
<select
bind:value={categoryFilter}
class="min-w-40 rounded-lg border border-gray-300 bg-white px-3 py-2 pr-8 text-sm outline-none transition-shadow focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
>
<option value="">All Categories</option>
{#each categories as category}
<option value={category}>{category}</option>
{/each}
</select>
<select
bind:value={frequencyFilter}
class="min-w-40 rounded-lg border border-gray-300 bg-white px-3 py-2 pr-8 text-sm outline-none transition-shadow focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
>
<option value="">All Frequencies</option>
{#each frequencies as frequency}
<option value={frequency}>{frequency}</option>
{/each}
</select>
{#if hasActiveFilters}
<button
type="button"
onclick={clearFilters}
class="rounded-lg px-3 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-700"
>
Clear filters
</button>
{/if}
<span class="ml-auto text-sm text-gray-500">
{filteredAndSorted().length} of {affirmations.length}
</span>
</div>
<div class="overflow-x-auto">
<table class="w-full border-collapse text-left text-sm">
<thead>
<tr class="border-b border-gray-200 bg-gray-50">
<th class="px-4 py-3 font-medium text-gray-700">
<button
type="button"
class="flex items-center gap-1 hover:text-gray-900"
onclick={() => toggleSort('name')}
>
Name
<span class="text-xs text-gray-400">{getSortIndicator('name')}</span>
</button>
</th>
<th class="px-4 py-3 font-medium text-gray-700">
<button
type="button"
class="flex items-center gap-1 hover:text-gray-900"
onclick={() => toggleSort('category')}
>
Category
<span class="text-xs text-gray-400">{getSortIndicator('category')}</span>
</button>
</th>
<th class="px-4 py-3 font-medium text-gray-700">
<button
type="button"
class="flex items-center gap-1 hover:text-gray-900"
onclick={() => toggleSort('frequency')}
>
Frequency
<span class="text-xs text-gray-400">{getSortIndicator('frequency')}</span>
</button>
</th>
<th class="px-4 py-3 text-right font-medium text-gray-700">
<button
type="button"
class="ml-auto flex items-center gap-1 hover:text-gray-900"
onclick={() => toggleSort('price')}
>
Price
<span class="text-xs text-gray-400">{getSortIndicator('price')}</span>
</button>
</th>
<th class="px-4 py-3 font-medium text-gray-700"></th>
</tr>
</thead>
<tbody>
{#each filteredAndSorted() as affirmation}
<tr class="border-b border-gray-100 hover:bg-gray-50">
<td class="px-4 py-3 font-medium text-gray-900">{affirmation.name}</td>
<td class="px-4 py-3 text-gray-600">{categoryDisplayName(affirmation.category) || '—'}</td>
<td class="px-4 py-3 text-gray-600">{affirmation.solfeggioFrequency?.name ?? '—'}</td>
<td class="px-4 py-3 text-right text-gray-600">${affirmation.price.toFixed(2)}</td>
<td class="px-4 py-3 text-right">
<a
href="/affirmations/{affirmation.id}"
class="text-blue-600 hover:text-blue-800 hover:underline"
>
Edit
</a>
</td>
</tr>
{:else}
<tr>
<td colspan="5" class="px-4 py-8 text-center text-gray-500">
{#if hasActiveFilters}
No affirmations match your filters
{:else}
No affirmations yet
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
@@ -1,176 +0,0 @@
<script lang="ts">
type Frequency = {
id: number;
name: string;
value: number;
_count?: {
affirmations: number;
};
};
type SortKey = 'name' | 'value' | 'affirmations';
type SortDirection = 'asc' | 'desc';
type Props = {
frequencies: Frequency[];
};
let { frequencies }: Props = $props();
let search = $state('');
let sortKey = $state<SortKey>('name');
let sortDirection = $state<SortDirection>('asc');
const filteredAndSorted = $derived(() => {
let result = frequencies;
if (search) {
const searchLower = search.toLowerCase();
result = result.filter(
(f) =>
f.name.toLowerCase().includes(searchLower) ||
f.value.toString().includes(searchLower)
);
}
result = [...result].sort((a, b) => {
let aVal: string | number;
let bVal: string | number;
switch (sortKey) {
case 'name':
aVal = a.name.toLowerCase();
bVal = b.name.toLowerCase();
break;
case 'value':
aVal = a.value;
bVal = b.value;
break;
case 'affirmations':
aVal = a._count?.affirmations ?? 0;
bVal = b._count?.affirmations ?? 0;
break;
}
if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1;
if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1;
return 0;
});
return result;
});
function toggleSort(key: SortKey) {
if (sortKey === key) {
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
} else {
sortKey = key;
sortDirection = 'asc';
}
}
function getSortIndicator(key: SortKey) {
if (sortKey !== key) return '\u21D5';
return sortDirection === 'asc' ? '\u2191' : '\u2193';
}
function clearFilters() {
search = '';
}
const hasActiveFilters = $derived(!!search);
</script>
<div class="flex flex-col gap-4">
<div class="flex flex-wrap items-center gap-3">
<input
type="text"
placeholder="Search frequencies..."
bind:value={search}
class="rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none transition-shadow focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
/>
{#if hasActiveFilters}
<button
type="button"
onclick={clearFilters}
class="rounded-lg px-3 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-700"
>
Clear filters
</button>
{/if}
<span class="ml-auto text-sm text-gray-500">
{filteredAndSorted().length} of {frequencies.length}
</span>
</div>
<div class="overflow-x-auto">
<table class="w-full border-collapse text-left text-sm">
<thead>
<tr class="border-b border-gray-200 bg-gray-50">
<th class="px-4 py-3 font-medium text-gray-700">
<button
type="button"
class="flex items-center gap-1 hover:text-gray-900"
onclick={() => toggleSort('name')}
>
Name
<span class="text-xs text-gray-400">{getSortIndicator('name')}</span>
</button>
</th>
<th class="px-4 py-3 text-right font-medium text-gray-700">
<button
type="button"
class="ml-auto flex items-center gap-1 hover:text-gray-900"
onclick={() => toggleSort('value')}
>
Frequency (Hz)
<span class="text-xs text-gray-400">{getSortIndicator('value')}</span>
</button>
</th>
<th class="px-4 py-3 text-right font-medium text-gray-700">
<button
type="button"
class="ml-auto flex items-center gap-1 hover:text-gray-900"
onclick={() => toggleSort('affirmations')}
>
Affirmations
<span class="text-xs text-gray-400">{getSortIndicator('affirmations')}</span>
</button>
</th>
<th class="px-4 py-3 font-medium text-gray-700"></th>
</tr>
</thead>
<tbody>
{#each filteredAndSorted() as frequency}
<tr class="border-b border-gray-100 hover:bg-gray-50">
<td class="px-4 py-3 font-medium text-gray-900">{frequency.name}</td>
<td class="px-4 py-3 text-right text-gray-600">{frequency.value} Hz</td>
<td class="px-4 py-3 text-right text-gray-600">
{frequency._count?.affirmations ?? 0}
</td>
<td class="px-4 py-3 text-right">
<a
href="/frequencies/{frequency.id}"
class="text-blue-600 hover:text-blue-800 hover:underline"
>
Edit
</a>
</td>
</tr>
{:else}
<tr>
<td colspan="4" class="px-4 py-8 text-center text-gray-500">
{#if hasActiveFilters}
No frequencies match your filters
{:else}
No frequencies yet
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
+8 -1
View File
@@ -1,15 +1,22 @@
<script lang="ts">
import { authClient } from '$lib/auth-client';
import { canAccessAdmin } from '$lib/svelte-admin/roles';
let { userRoleId = null }: { userRoleId: number | null } = $props();
const session = authClient.useSession();
let isAdmin = $derived(canAccessAdmin(userRoleId));
</script>
<nav class="flex w-full justify-between px-8 py-6">
<span><a href="/">Rhema Wave Technologies</a></span>
{#if $session.data}
<div>
{#if isAdmin}
<a href="/admin" class="mx-2">Admin Panel</a>
{/if}
<a href="/dashboard" class="mx-2">Dashboard</a>
<button
on:click={async () => {
onclick={async () => {
authClient.signOut();
}}>Log Out</button
>
+1 -1
View File
@@ -28,6 +28,6 @@ function getPrisma(): PrismaClient {
export const prisma = new Proxy({} as PrismaClient, {
get(_, prop) {
return (getPrisma() as Record<string | symbol, unknown>)[prop];
return (getPrisma() as unknown as Record<string | symbol, unknown>)[prop];
}
});
+4 -4
View File
@@ -53,20 +53,20 @@ export type UserRole = Prisma.UserRoleModel
*/
export type Verification = Prisma.VerificationModel
/**
* Model Affirmation
* Model Proclamation
*
*/
export type Affirmation = Prisma.AffirmationModel
export type Proclamation = Prisma.ProclamationModel
/**
* Model SolfeggioFrequency
*
*/
export type SolfeggioFrequency = Prisma.SolfeggioFrequencyModel
/**
* Model AffirmationCategory
* Model ProclamationCategory
*
*/
export type AffirmationCategory = Prisma.AffirmationCategoryModel
export type ProclamationCategory = Prisma.ProclamationCategoryModel
/**
* Model LicenseKey
*
+4 -4
View File
@@ -75,20 +75,20 @@ export type UserRole = Prisma.UserRoleModel
*/
export type Verification = Prisma.VerificationModel
/**
* Model Affirmation
* Model Proclamation
*
*/
export type Affirmation = Prisma.AffirmationModel
export type Proclamation = Prisma.ProclamationModel
/**
* Model SolfeggioFrequency
*
*/
export type SolfeggioFrequency = Prisma.SolfeggioFrequencyModel
/**
* Model AffirmationCategory
* Model ProclamationCategory
*
*/
export type AffirmationCategory = Prisma.AffirmationCategoryModel
export type ProclamationCategory = Prisma.ProclamationCategoryModel
/**
* Model LicenseKey
*
File diff suppressed because one or more lines are too long
@@ -391,9 +391,9 @@ export const ModelName = {
Account: 'Account',
UserRole: 'UserRole',
Verification: 'Verification',
Affirmation: 'Affirmation',
Proclamation: 'Proclamation',
SolfeggioFrequency: 'SolfeggioFrequency',
AffirmationCategory: 'AffirmationCategory',
ProclamationCategory: 'ProclamationCategory',
LicenseKey: 'LicenseKey'
} as const
@@ -410,7 +410,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
omit: GlobalOmitOptions
}
meta: {
modelProps: "user" | "profile" | "address" | "session" | "account" | "userRole" | "verification" | "affirmation" | "solfeggioFrequency" | "affirmationCategory" | "licenseKey"
modelProps: "user" | "profile" | "address" | "session" | "account" | "userRole" | "verification" | "proclamation" | "solfeggioFrequency" | "proclamationCategory" | "licenseKey"
txIsolationLevel: TransactionIsolationLevel
}
model: {
@@ -932,77 +932,77 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
}
}
}
Affirmation: {
payload: Prisma.$AffirmationPayload<ExtArgs>
fields: Prisma.AffirmationFieldRefs
Proclamation: {
payload: Prisma.$ProclamationPayload<ExtArgs>
fields: Prisma.ProclamationFieldRefs
operations: {
findUnique: {
args: Prisma.AffirmationFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AffirmationPayload> | null
args: Prisma.ProclamationFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationPayload> | null
}
findUniqueOrThrow: {
args: Prisma.AffirmationFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AffirmationPayload>
args: Prisma.ProclamationFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationPayload>
}
findFirst: {
args: Prisma.AffirmationFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AffirmationPayload> | null
args: Prisma.ProclamationFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationPayload> | null
}
findFirstOrThrow: {
args: Prisma.AffirmationFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AffirmationPayload>
args: Prisma.ProclamationFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationPayload>
}
findMany: {
args: Prisma.AffirmationFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AffirmationPayload>[]
args: Prisma.ProclamationFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationPayload>[]
}
create: {
args: Prisma.AffirmationCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AffirmationPayload>
args: Prisma.ProclamationCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationPayload>
}
createMany: {
args: Prisma.AffirmationCreateManyArgs<ExtArgs>
args: Prisma.ProclamationCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.AffirmationCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AffirmationPayload>[]
args: Prisma.ProclamationCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationPayload>[]
}
delete: {
args: Prisma.AffirmationDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AffirmationPayload>
args: Prisma.ProclamationDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationPayload>
}
update: {
args: Prisma.AffirmationUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AffirmationPayload>
args: Prisma.ProclamationUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationPayload>
}
deleteMany: {
args: Prisma.AffirmationDeleteManyArgs<ExtArgs>
args: Prisma.ProclamationDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.AffirmationUpdateManyArgs<ExtArgs>
args: Prisma.ProclamationUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.AffirmationUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AffirmationPayload>[]
args: Prisma.ProclamationUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationPayload>[]
}
upsert: {
args: Prisma.AffirmationUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AffirmationPayload>
args: Prisma.ProclamationUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationPayload>
}
aggregate: {
args: Prisma.AffirmationAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateAffirmation>
args: Prisma.ProclamationAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateProclamation>
}
groupBy: {
args: Prisma.AffirmationGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AffirmationGroupByOutputType>[]
args: Prisma.ProclamationGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.ProclamationGroupByOutputType>[]
}
count: {
args: Prisma.AffirmationCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AffirmationCountAggregateOutputType> | number
args: Prisma.ProclamationCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.ProclamationCountAggregateOutputType> | number
}
}
}
@@ -1080,77 +1080,77 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
}
}
}
AffirmationCategory: {
payload: Prisma.$AffirmationCategoryPayload<ExtArgs>
fields: Prisma.AffirmationCategoryFieldRefs
ProclamationCategory: {
payload: Prisma.$ProclamationCategoryPayload<ExtArgs>
fields: Prisma.ProclamationCategoryFieldRefs
operations: {
findUnique: {
args: Prisma.AffirmationCategoryFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AffirmationCategoryPayload> | null
args: Prisma.ProclamationCategoryFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategoryPayload> | null
}
findUniqueOrThrow: {
args: Prisma.AffirmationCategoryFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AffirmationCategoryPayload>
args: Prisma.ProclamationCategoryFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategoryPayload>
}
findFirst: {
args: Prisma.AffirmationCategoryFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AffirmationCategoryPayload> | null
args: Prisma.ProclamationCategoryFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategoryPayload> | null
}
findFirstOrThrow: {
args: Prisma.AffirmationCategoryFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AffirmationCategoryPayload>
args: Prisma.ProclamationCategoryFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategoryPayload>
}
findMany: {
args: Prisma.AffirmationCategoryFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AffirmationCategoryPayload>[]
args: Prisma.ProclamationCategoryFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategoryPayload>[]
}
create: {
args: Prisma.AffirmationCategoryCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AffirmationCategoryPayload>
args: Prisma.ProclamationCategoryCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategoryPayload>
}
createMany: {
args: Prisma.AffirmationCategoryCreateManyArgs<ExtArgs>
args: Prisma.ProclamationCategoryCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.AffirmationCategoryCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AffirmationCategoryPayload>[]
args: Prisma.ProclamationCategoryCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategoryPayload>[]
}
delete: {
args: Prisma.AffirmationCategoryDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AffirmationCategoryPayload>
args: Prisma.ProclamationCategoryDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategoryPayload>
}
update: {
args: Prisma.AffirmationCategoryUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AffirmationCategoryPayload>
args: Prisma.ProclamationCategoryUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategoryPayload>
}
deleteMany: {
args: Prisma.AffirmationCategoryDeleteManyArgs<ExtArgs>
args: Prisma.ProclamationCategoryDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.AffirmationCategoryUpdateManyArgs<ExtArgs>
args: Prisma.ProclamationCategoryUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.AffirmationCategoryUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AffirmationCategoryPayload>[]
args: Prisma.ProclamationCategoryUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategoryPayload>[]
}
upsert: {
args: Prisma.AffirmationCategoryUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AffirmationCategoryPayload>
args: Prisma.ProclamationCategoryUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategoryPayload>
}
aggregate: {
args: Prisma.AffirmationCategoryAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateAffirmationCategory>
args: Prisma.ProclamationCategoryAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateProclamationCategory>
}
groupBy: {
args: Prisma.AffirmationCategoryGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AffirmationCategoryGroupByOutputType>[]
args: Prisma.ProclamationCategoryGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.ProclamationCategoryGroupByOutputType>[]
}
count: {
args: Prisma.AffirmationCategoryCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AffirmationCategoryCountAggregateOutputType> | number
args: Prisma.ProclamationCategoryCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.ProclamationCategoryCountAggregateOutputType> | number
}
}
}
@@ -1358,7 +1358,7 @@ export const VerificationScalarFieldEnum = {
export type VerificationScalarFieldEnum = (typeof VerificationScalarFieldEnum)[keyof typeof VerificationScalarFieldEnum]
export const AffirmationScalarFieldEnum = {
export const ProclamationScalarFieldEnum = {
id: 'id',
name: 'name',
content: 'content',
@@ -1367,7 +1367,7 @@ export const AffirmationScalarFieldEnum = {
price: 'price'
} as const
export type AffirmationScalarFieldEnum = (typeof AffirmationScalarFieldEnum)[keyof typeof AffirmationScalarFieldEnum]
export type ProclamationScalarFieldEnum = (typeof ProclamationScalarFieldEnum)[keyof typeof ProclamationScalarFieldEnum]
export const SolfeggioFrequencyScalarFieldEnum = {
@@ -1379,13 +1379,13 @@ export const SolfeggioFrequencyScalarFieldEnum = {
export type SolfeggioFrequencyScalarFieldEnum = (typeof SolfeggioFrequencyScalarFieldEnum)[keyof typeof SolfeggioFrequencyScalarFieldEnum]
export const AffirmationCategoryScalarFieldEnum = {
export const ProclamationCategoryScalarFieldEnum = {
id: 'id',
name: 'name',
parentId: 'parentId'
} as const
export type AffirmationCategoryScalarFieldEnum = (typeof AffirmationCategoryScalarFieldEnum)[keyof typeof AffirmationCategoryScalarFieldEnum]
export type ProclamationCategoryScalarFieldEnum = (typeof ProclamationCategoryScalarFieldEnum)[keyof typeof ProclamationCategoryScalarFieldEnum]
export const LicenseKeyScalarFieldEnum = {
@@ -1593,9 +1593,9 @@ export type GlobalOmitConfig = {
account?: Prisma.AccountOmit
userRole?: Prisma.UserRoleOmit
verification?: Prisma.VerificationOmit
affirmation?: Prisma.AffirmationOmit
proclamation?: Prisma.ProclamationOmit
solfeggioFrequency?: Prisma.SolfeggioFrequencyOmit
affirmationCategory?: Prisma.AffirmationCategoryOmit
proclamationCategory?: Prisma.ProclamationCategoryOmit
licenseKey?: Prisma.LicenseKeyOmit
}
@@ -58,9 +58,9 @@ export const ModelName = {
Account: 'Account',
UserRole: 'UserRole',
Verification: 'Verification',
Affirmation: 'Affirmation',
Proclamation: 'Proclamation',
SolfeggioFrequency: 'SolfeggioFrequency',
AffirmationCategory: 'AffirmationCategory',
ProclamationCategory: 'ProclamationCategory',
LicenseKey: 'LicenseKey'
} as const
@@ -171,7 +171,7 @@ export const VerificationScalarFieldEnum = {
export type VerificationScalarFieldEnum = (typeof VerificationScalarFieldEnum)[keyof typeof VerificationScalarFieldEnum]
export const AffirmationScalarFieldEnum = {
export const ProclamationScalarFieldEnum = {
id: 'id',
name: 'name',
content: 'content',
@@ -180,7 +180,7 @@ export const AffirmationScalarFieldEnum = {
price: 'price'
} as const
export type AffirmationScalarFieldEnum = (typeof AffirmationScalarFieldEnum)[keyof typeof AffirmationScalarFieldEnum]
export type ProclamationScalarFieldEnum = (typeof ProclamationScalarFieldEnum)[keyof typeof ProclamationScalarFieldEnum]
export const SolfeggioFrequencyScalarFieldEnum = {
@@ -192,13 +192,13 @@ export const SolfeggioFrequencyScalarFieldEnum = {
export type SolfeggioFrequencyScalarFieldEnum = (typeof SolfeggioFrequencyScalarFieldEnum)[keyof typeof SolfeggioFrequencyScalarFieldEnum]
export const AffirmationCategoryScalarFieldEnum = {
export const ProclamationCategoryScalarFieldEnum = {
id: 'id',
name: 'name',
parentId: 'parentId'
} as const
export type AffirmationCategoryScalarFieldEnum = (typeof AffirmationCategoryScalarFieldEnum)[keyof typeof AffirmationCategoryScalarFieldEnum]
export type ProclamationCategoryScalarFieldEnum = (typeof ProclamationCategoryScalarFieldEnum)[keyof typeof ProclamationCategoryScalarFieldEnum]
export const LicenseKeyScalarFieldEnum = {
+2 -2
View File
@@ -15,8 +15,8 @@ export type * from './models/Session.ts'
export type * from './models/Account.ts'
export type * from './models/UserRole.ts'
export type * from './models/Verification.ts'
export type * from './models/Affirmation.ts'
export type * from './models/Proclamation.ts'
export type * from './models/SolfeggioFrequency.ts'
export type * from './models/AffirmationCategory.ts'
export type * from './models/ProclamationCategory.ts'
export type * from './models/LicenseKey.ts'
export type * from './commonInputTypes.ts'
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -204,14 +204,14 @@ export type SolfeggioFrequencyWhereInput = {
id?: Prisma.IntFilter<"SolfeggioFrequency"> | number
name?: Prisma.StringFilter<"SolfeggioFrequency"> | string
value?: Prisma.FloatFilter<"SolfeggioFrequency"> | number
affirmations?: Prisma.AffirmationListRelationFilter
proclamations?: Prisma.ProclamationListRelationFilter
}
export type SolfeggioFrequencyOrderByWithRelationInput = {
id?: Prisma.SortOrder
name?: Prisma.SortOrder
value?: Prisma.SortOrder
affirmations?: Prisma.AffirmationOrderByRelationAggregateInput
proclamations?: Prisma.ProclamationOrderByRelationAggregateInput
}
export type SolfeggioFrequencyWhereUniqueInput = Prisma.AtLeast<{
@@ -221,7 +221,7 @@ export type SolfeggioFrequencyWhereUniqueInput = Prisma.AtLeast<{
AND?: Prisma.SolfeggioFrequencyWhereInput | Prisma.SolfeggioFrequencyWhereInput[]
OR?: Prisma.SolfeggioFrequencyWhereInput[]
NOT?: Prisma.SolfeggioFrequencyWhereInput | Prisma.SolfeggioFrequencyWhereInput[]
affirmations?: Prisma.AffirmationListRelationFilter
proclamations?: Prisma.ProclamationListRelationFilter
}, "id" | "name" | "value">
export type SolfeggioFrequencyOrderByWithAggregationInput = {
@@ -247,27 +247,27 @@ export type SolfeggioFrequencyScalarWhereWithAggregatesInput = {
export type SolfeggioFrequencyCreateInput = {
name: string
value: number
affirmations?: Prisma.AffirmationCreateNestedManyWithoutSolfeggioFrequencyInput
proclamations?: Prisma.ProclamationCreateNestedManyWithoutSolfeggioFrequencyInput
}
export type SolfeggioFrequencyUncheckedCreateInput = {
id?: number
name: string
value: number
affirmations?: Prisma.AffirmationUncheckedCreateNestedManyWithoutSolfeggioFrequencyInput
proclamations?: Prisma.ProclamationUncheckedCreateNestedManyWithoutSolfeggioFrequencyInput
}
export type SolfeggioFrequencyUpdateInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
value?: Prisma.FloatFieldUpdateOperationsInput | number
affirmations?: Prisma.AffirmationUpdateManyWithoutSolfeggioFrequencyNestedInput
proclamations?: Prisma.ProclamationUpdateManyWithoutSolfeggioFrequencyNestedInput
}
export type SolfeggioFrequencyUncheckedUpdateInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number
name?: Prisma.StringFieldUpdateOperationsInput | string
value?: Prisma.FloatFieldUpdateOperationsInput | number
affirmations?: Prisma.AffirmationUncheckedUpdateManyWithoutSolfeggioFrequencyNestedInput
proclamations?: Prisma.ProclamationUncheckedUpdateManyWithoutSolfeggioFrequencyNestedInput
}
export type SolfeggioFrequencyCreateManyInput = {
@@ -320,53 +320,53 @@ export type SolfeggioFrequencySumOrderByAggregateInput = {
value?: Prisma.SortOrder
}
export type SolfeggioFrequencyCreateNestedOneWithoutAffirmationsInput = {
create?: Prisma.XOR<Prisma.SolfeggioFrequencyCreateWithoutAffirmationsInput, Prisma.SolfeggioFrequencyUncheckedCreateWithoutAffirmationsInput>
connectOrCreate?: Prisma.SolfeggioFrequencyCreateOrConnectWithoutAffirmationsInput
export type SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput = {
create?: Prisma.XOR<Prisma.SolfeggioFrequencyCreateWithoutProclamationsInput, Prisma.SolfeggioFrequencyUncheckedCreateWithoutProclamationsInput>
connectOrCreate?: Prisma.SolfeggioFrequencyCreateOrConnectWithoutProclamationsInput
connect?: Prisma.SolfeggioFrequencyWhereUniqueInput
}
export type SolfeggioFrequencyUpdateOneRequiredWithoutAffirmationsNestedInput = {
create?: Prisma.XOR<Prisma.SolfeggioFrequencyCreateWithoutAffirmationsInput, Prisma.SolfeggioFrequencyUncheckedCreateWithoutAffirmationsInput>
connectOrCreate?: Prisma.SolfeggioFrequencyCreateOrConnectWithoutAffirmationsInput
upsert?: Prisma.SolfeggioFrequencyUpsertWithoutAffirmationsInput
export type SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput = {
create?: Prisma.XOR<Prisma.SolfeggioFrequencyCreateWithoutProclamationsInput, Prisma.SolfeggioFrequencyUncheckedCreateWithoutProclamationsInput>
connectOrCreate?: Prisma.SolfeggioFrequencyCreateOrConnectWithoutProclamationsInput
upsert?: Prisma.SolfeggioFrequencyUpsertWithoutProclamationsInput
connect?: Prisma.SolfeggioFrequencyWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.SolfeggioFrequencyUpdateToOneWithWhereWithoutAffirmationsInput, Prisma.SolfeggioFrequencyUpdateWithoutAffirmationsInput>, Prisma.SolfeggioFrequencyUncheckedUpdateWithoutAffirmationsInput>
update?: Prisma.XOR<Prisma.XOR<Prisma.SolfeggioFrequencyUpdateToOneWithWhereWithoutProclamationsInput, Prisma.SolfeggioFrequencyUpdateWithoutProclamationsInput>, Prisma.SolfeggioFrequencyUncheckedUpdateWithoutProclamationsInput>
}
export type SolfeggioFrequencyCreateWithoutAffirmationsInput = {
export type SolfeggioFrequencyCreateWithoutProclamationsInput = {
name: string
value: number
}
export type SolfeggioFrequencyUncheckedCreateWithoutAffirmationsInput = {
export type SolfeggioFrequencyUncheckedCreateWithoutProclamationsInput = {
id?: number
name: string
value: number
}
export type SolfeggioFrequencyCreateOrConnectWithoutAffirmationsInput = {
export type SolfeggioFrequencyCreateOrConnectWithoutProclamationsInput = {
where: Prisma.SolfeggioFrequencyWhereUniqueInput
create: Prisma.XOR<Prisma.SolfeggioFrequencyCreateWithoutAffirmationsInput, Prisma.SolfeggioFrequencyUncheckedCreateWithoutAffirmationsInput>
create: Prisma.XOR<Prisma.SolfeggioFrequencyCreateWithoutProclamationsInput, Prisma.SolfeggioFrequencyUncheckedCreateWithoutProclamationsInput>
}
export type SolfeggioFrequencyUpsertWithoutAffirmationsInput = {
update: Prisma.XOR<Prisma.SolfeggioFrequencyUpdateWithoutAffirmationsInput, Prisma.SolfeggioFrequencyUncheckedUpdateWithoutAffirmationsInput>
create: Prisma.XOR<Prisma.SolfeggioFrequencyCreateWithoutAffirmationsInput, Prisma.SolfeggioFrequencyUncheckedCreateWithoutAffirmationsInput>
export type SolfeggioFrequencyUpsertWithoutProclamationsInput = {
update: Prisma.XOR<Prisma.SolfeggioFrequencyUpdateWithoutProclamationsInput, Prisma.SolfeggioFrequencyUncheckedUpdateWithoutProclamationsInput>
create: Prisma.XOR<Prisma.SolfeggioFrequencyCreateWithoutProclamationsInput, Prisma.SolfeggioFrequencyUncheckedCreateWithoutProclamationsInput>
where?: Prisma.SolfeggioFrequencyWhereInput
}
export type SolfeggioFrequencyUpdateToOneWithWhereWithoutAffirmationsInput = {
export type SolfeggioFrequencyUpdateToOneWithWhereWithoutProclamationsInput = {
where?: Prisma.SolfeggioFrequencyWhereInput
data: Prisma.XOR<Prisma.SolfeggioFrequencyUpdateWithoutAffirmationsInput, Prisma.SolfeggioFrequencyUncheckedUpdateWithoutAffirmationsInput>
data: Prisma.XOR<Prisma.SolfeggioFrequencyUpdateWithoutProclamationsInput, Prisma.SolfeggioFrequencyUncheckedUpdateWithoutProclamationsInput>
}
export type SolfeggioFrequencyUpdateWithoutAffirmationsInput = {
export type SolfeggioFrequencyUpdateWithoutProclamationsInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
value?: Prisma.FloatFieldUpdateOperationsInput | number
}
export type SolfeggioFrequencyUncheckedUpdateWithoutAffirmationsInput = {
export type SolfeggioFrequencyUncheckedUpdateWithoutProclamationsInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number
name?: Prisma.StringFieldUpdateOperationsInput | string
value?: Prisma.FloatFieldUpdateOperationsInput | number
@@ -378,11 +378,11 @@ export type SolfeggioFrequencyUncheckedUpdateWithoutAffirmationsInput = {
*/
export type SolfeggioFrequencyCountOutputType = {
affirmations: number
proclamations: number
}
export type SolfeggioFrequencyCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
affirmations?: boolean | SolfeggioFrequencyCountOutputTypeCountAffirmationsArgs
proclamations?: boolean | SolfeggioFrequencyCountOutputTypeCountProclamationsArgs
}
/**
@@ -398,8 +398,8 @@ export type SolfeggioFrequencyCountOutputTypeDefaultArgs<ExtArgs extends runtime
/**
* SolfeggioFrequencyCountOutputType without action
*/
export type SolfeggioFrequencyCountOutputTypeCountAffirmationsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.AffirmationWhereInput
export type SolfeggioFrequencyCountOutputTypeCountProclamationsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.ProclamationWhereInput
}
@@ -407,7 +407,7 @@ export type SolfeggioFrequencySelect<ExtArgs extends runtime.Types.Extensions.In
id?: boolean
name?: boolean
value?: boolean
affirmations?: boolean | Prisma.SolfeggioFrequency$affirmationsArgs<ExtArgs>
proclamations?: boolean | Prisma.SolfeggioFrequency$proclamationsArgs<ExtArgs>
_count?: boolean | Prisma.SolfeggioFrequencyCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["solfeggioFrequency"]>
@@ -431,7 +431,7 @@ export type SolfeggioFrequencySelectScalar = {
export type SolfeggioFrequencyOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "value", ExtArgs["result"]["solfeggioFrequency"]>
export type SolfeggioFrequencyInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
affirmations?: boolean | Prisma.SolfeggioFrequency$affirmationsArgs<ExtArgs>
proclamations?: boolean | Prisma.SolfeggioFrequency$proclamationsArgs<ExtArgs>
_count?: boolean | Prisma.SolfeggioFrequencyCountOutputTypeDefaultArgs<ExtArgs>
}
export type SolfeggioFrequencyIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {}
@@ -440,7 +440,7 @@ export type SolfeggioFrequencyIncludeUpdateManyAndReturn<ExtArgs extends runtime
export type $SolfeggioFrequencyPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "SolfeggioFrequency"
objects: {
affirmations: Prisma.$AffirmationPayload<ExtArgs>[]
proclamations: Prisma.$ProclamationPayload<ExtArgs>[]
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: number
@@ -840,7 +840,7 @@ readonly fields: SolfeggioFrequencyFieldRefs;
*/
export interface Prisma__SolfeggioFrequencyClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
readonly [Symbol.toStringTag]: "PrismaPromise"
affirmations<T extends Prisma.SolfeggioFrequency$affirmationsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.SolfeggioFrequency$affirmationsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$AffirmationPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
proclamations<T extends Prisma.SolfeggioFrequency$proclamationsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.SolfeggioFrequency$proclamationsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ProclamationPayload<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.
@@ -1261,27 +1261,27 @@ export type SolfeggioFrequencyDeleteManyArgs<ExtArgs extends runtime.Types.Exten
}
/**
* SolfeggioFrequency.affirmations
* SolfeggioFrequency.proclamations
*/
export type SolfeggioFrequency$affirmationsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
export type SolfeggioFrequency$proclamationsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Affirmation
* Select specific fields to fetch from the Proclamation
*/
select?: Prisma.AffirmationSelect<ExtArgs> | null
select?: Prisma.ProclamationSelect<ExtArgs> | null
/**
* Omit specific fields from the Affirmation
* Omit specific fields from the Proclamation
*/
omit?: Prisma.AffirmationOmit<ExtArgs> | null
omit?: Prisma.ProclamationOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.AffirmationInclude<ExtArgs> | null
where?: Prisma.AffirmationWhereInput
orderBy?: Prisma.AffirmationOrderByWithRelationInput | Prisma.AffirmationOrderByWithRelationInput[]
cursor?: Prisma.AffirmationWhereUniqueInput
include?: Prisma.ProclamationInclude<ExtArgs> | null
where?: Prisma.ProclamationWhereInput
orderBy?: Prisma.ProclamationOrderByWithRelationInput | Prisma.ProclamationOrderByWithRelationInput[]
cursor?: Prisma.ProclamationWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.AffirmationScalarFieldEnum | Prisma.AffirmationScalarFieldEnum[]
distinct?: Prisma.ProclamationScalarFieldEnum | Prisma.ProclamationScalarFieldEnum[]
}
/**
+318 -142
View File
@@ -1,194 +1,370 @@
# svelte-admin
A lightweight, configuration-driven admin panel framework for SvelteKit + Prisma applications. Models are registered once in a central registry and automatically get list views, create modals, edit pages, and delete functionality — all wired to Prisma via dynamic delegates.
A config-driven admin panel framework for SvelteKit + Prisma. Define your models once in a
registry and get full CRUD UI with dynamic routing, role-based access control, searchable
dropdowns, and related-record panels — no per-model route files needed.
## Architecture
```
src/lib/svelte-admin/ # Framework core
types.ts # AdminModelConfig type + supporting types
registry.ts # Model configurations (the main config file)
roles.ts # Role constants + access helpers
introspect.ts # Field label formatting + nested value resolution
prisma-delegate.ts # Dynamic Prisma delegate lookup + ID parsing
form-data.ts # FormData → typed object extraction
index.ts # Barrel re-exports
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
└── index.ts # Barrel exports
src/lib/components/admin/ # UI components
AdminTable.svelte # Sortable, searchable list table
AdminForm.svelte # Dynamic form (creates inputs from field config)
src/routes/(protected)/admin/ # Dynamic admin routes
├── +layout.server.ts # Role-based access guard
├── +layout.svelte # Layout shell
├── +page.server.ts # Admin home (model grid)
├── +page.svelte # Admin home UI
├── [model]/
│ ├── +page.server.ts # List view + create action
│ ├── +page.svelte # List view UI
│ └── [id]/
│ ├── +page.server.ts # Edit view + update/delete actions
│ └── +page.svelte # Edit view UI
src/routes/(protected)/admin/ # SvelteKit routes
+layout.server.ts # Auth guard (requires STAFF role or higher)
+layout.svelte # Admin shell layout
+page.svelte # Home grid (cards linking to each model)
+page.server.ts # Loads all model configs for the home grid
[model]/+page.svelte # List view + create modal for a model
[model]/+page.server.ts # List loader + create action
[model]/[id]/+page.svelte # Edit view + delete for a single record
[model]/[id]/+page.server.ts # Record loader + update/delete actions
src/lib/components/admin/ # Reusable admin UI components
├── AdminTable.svelte # Sortable, searchable data table
├── AdminForm.svelte # Dynamic form with type-aware inputs
├── ComboBox.svelte # Searchable dropdown for FK fields
├── RelatedHasOne.svelte # Display panel for 1:1 related records
└── RelatedHasMany.svelte # Display table for 1:N related records
```
## Adding a New Model
1. Open `src/lib/svelte-admin/registry.ts`.
2. Add a new object to the `models` array:
1. **Define the Prisma model** in `prisma/schema.prisma` and create a migration:
```ts
{
slug: 'widgets', // URL segment: /admin/widgets
modelName: 'Widget', // Prisma model name (PascalCase)
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',
},
},
}
```
```bash
npm run migrate -- --name add_my_model
```
3. That's it. The dynamic `[model]` routes handle everything else.
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. **Done.** Visit `/admin/widgets`. The dynamic `[model]` routes handle list, create, edit,
and delete automatically.
No new route files, no new components. The registry is the only thing you touch.
## Configuration Reference
### `AdminModelConfig`
### AdminModelConfig
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `slug` | `string` | Yes | URL path segment. Use kebab-case (e.g. `'license-keys'`). |
| `modelName` | `string` | Yes | Prisma model name in PascalCase (e.g. `'LicenseKey'`). Used to look up the Prisma delegate. |
| `displayName` | `string` | Yes | Plural name shown in headings and the admin home grid. |
| `displayNameSingular` | `string` | Yes | Singular name for create/edit headings and button labels. |
|---|---|---|---|
| `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>` | No | Prisma `include` clause for eager-loading relations in list and edit views. |
| `listFields` | `string[]` | Yes | Fields shown as columns in the list table. Supports dot-notation for relations (e.g. `'user.name'`). |
| `searchFields` | `string[]` | Yes | Fields searched by the table's search box. Should be scalar fields on the model. |
| `formFields` | `string[]` | Yes | Fields shown in the create/edit form. Order determines form field order. |
| `readonlyFields` | `string[]` | No | Fields shown as read-only text on the edit page (not editable). Typically `['id', 'createdAt', 'updatedAt']`. |
| `listOnly` | `boolean` | No | When `true`, suppresses edit links in the table. The create modal still works. Use for models where records are auto-generated or shouldn't be edited individually. |
| `fieldTypes` | `Record<string, PrismaScalarType>` | Yes | Maps field names to their Prisma scalar type. Controls form input type and form data parsing. |
| `relationOptions` | `Record<string, RelationOptionConfig>` | No | Configures dropdown selects for foreign key fields. |
| `accessFilter` | `(viewerRoleId: number) => Record<string, unknown> \| undefined` | No | Returns a Prisma `where` clause to filter list results based on the viewer's role. Return `undefined` for no filter. |
| `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. |
| `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 ComboBox dropdowns. |
| `relatedRecords` | `RelatedRecordConfig[]` | No | Has-one/has-many panels shown on edit pages. |
| `accessFilter` | `(viewerRoleId: number) => where \| undefined` | No | Returns a Prisma `where` clause to filter records by viewer role. `undefined` = no filter. |
### `PrismaScalarType`
### PrismaScalarType
`'String' | 'Int' | 'Float' | 'Boolean' | 'DateTime'`
Controls both the HTML input type in forms and how `FormData` values are parsed:
Controls both form input rendering and FormData parsing:
| Type | Input | Parsing |
|------|-------|---------|
| `String` | `<input type="text">` | `.toString()` |
| `Int` | `<input type="number" step="1">` | `parseInt()` |
| `Float` | `<input type="number" step="0.01">` | `parseFloat()` |
| 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()` |
| `DateTime` | `<input type="datetime-local">` | `new Date()`, empty → `null` |
### `RelationOptionConfig`
### RelationOptionConfig
```ts
{
model: 'UserRole', // Prisma model to query for options
labelField: 'name', // Field to display in the dropdown
valueField: 'id', // Field value submitted with the form
labelField: 'name', // Field displayed in the dropdown
valueField: 'id' // Field value submitted with the form
}
```
The framework runs `findMany` on the related model, selecting only `labelField` and `valueField`, and renders a `<select>` dropdown in 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 `ComboBox` (searchable
dropdown) instead of the default input for that type.
## Relations
To display a related field in the list table (e.g. show a user's role name):
### Foreign Key Dropdowns (relationOptions)
1. Add the relation to `include`: `{ role: true }`
2. Use dot-notation in `listFields`: `'role.name'`
To make a foreign key editable as a dropdown:
1. Add the FK field to `formFields`: `'roleId'`
2. Add its type to `fieldTypes`: `roleId: 'Int'`
3. Configure `relationOptions`:
```ts
relationOptions: {
roleId: { model: 'UserRole', labelField: 'name', valueField: 'id' }
}
```
## List-Only Models
Set `listOnly: true` on a model config to suppress edit links in the list table. Records still appear in the table and the create modal still works, but there's no way to click into an individual record to edit or delete it.
This is useful for models like license keys where records are auto-generated and shouldn't be manually edited.
## Read-Only Fields
Fields listed in `readonlyFields` are displayed as plain text on the edit page (above the form) but cannot be modified. Common use: `['id', 'createdAt', 'updatedAt']`.
If you want the edit page to show mostly read-only data with only one or two editable fields, move most fields from `formFields` to `readonlyFields`. For example, the Users config has `formFields: ['roleId']` with everything else in `readonlyFields`.
## Access Control
### Admin layout guard
The admin layout (`admin/+layout.server.ts`) checks `canAccessAdmin(user.roleId)` — which requires `roleId <= ROLES.STAFF` (i.e. Super, Admin, or Staff). Users with higher `roleId` values get a 403.
### Role constants
Defined in `roles.ts`:
When a form field is a foreign key, add a `relationOptions` entry to render a searchable ComboBox:
```ts
ROLES.SUPER = 1 // Full access
ROLES.ADMIN = 2 // Admin access
ROLES.STAFF = 3 // Staff access (minimum for admin panel)
ROLES.USER = 4 // Regular user (no admin access)
```
Lower numbers = higher privilege.
### Per-model filtering with `accessFilter`
The `accessFilter` function receives the viewer's `roleId` and returns a Prisma `where` clause that's merged into the list query. This lets you restrict which records different roles can see.
Example from the Users config:
```ts
accessFilter: (viewerRoleId: number) => {
if (viewerRoleId <= ROLES.SUPER) return undefined; // Super sees all
if (viewerRoleId <= ROLES.ADMIN) {
return { roleId: { gte: ROLES.STAFF } }; // Admin sees Staff+User
{
formFields: ['name', 'categoryId'],
fieldTypes: { name: 'String', categoryId: 'Int' },
relationOptions: {
categoryId: {
model: 'ProclamationCategory',
labelField: 'name',
valueField: 'id'
}
}
return { roleId: { gt: ROLES.STAFF } }; // Staff sees only Users
}
```
## Customizing the UI
The route loader automatically queries the related model and passes options to the ComboBox.
### 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. `has-one` renders
a key-value panel via `RelatedHasOne`; `has-many` renders a table via `RelatedHasMany`. Both
support an optional `editSlug` to link to the related record's edit page.
## 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; // no filter
if (viewerRoleId <= ROLES.ADMIN) {
return { roleId: { gte: ROLES.STAFF } }; // can't see other admins
}
return { roleId: { gt: ROLES.STAFF } }; // staff sees only regular users
}
```
## How Dynamic Routing Works
1. User visits `/admin/proclamations` (or `/admin/proclamations/abc123`)
2. SvelteKit matches the `[model]` (and `[id]`) route params
3. `+page.server.ts` calls `getModelConfig(params.model)` to look up the config
4. `getDelegate(config)` returns `prisma[lcfirst(config.modelName)]` (e.g., `prisma.proclamation`)
5. The delegate runs `findMany` (list), `findUnique` (edit), `create`, `update`, or `delete`
6. Data + config are passed to the Svelte page component
7. Components (`AdminTable`, `AdminForm`, etc.) render based on config
## Component API
### AdminTable
Props: `records`, `listFields`, `searchFields`, `modelSlug`, `listOnly?`
| Prop | Type | Description |
|---|---|---|
| `records` | `Record<string, unknown>[]` | Data rows |
| `listFields` | `string[]` | Column keys (dot-notation supported) |
| `searchFields` | `string[]` | Fields for client-side search |
| `modelSlug` | `string` | Used for building edit links |
| `listOnly` | `boolean?` | Suppress edit links |
Features:
- Client-side search across `searchFields`
- Click column headers to sort (ascending/descending toggle)
- Edit links per row (suppressed when `listOnly` is true)
Features: client-side search, click-to-sort columns, auto-formatted cells (dates, booleans,
decimals), filtered count display.
### AdminForm
Props: `fields`, `fieldTypes`, `relationOptions?`, `relationOptionsData?`, `record?`, `form?`, `action`, `submitLabel?`
| 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) |
| `form` | `ActionData?` | Carries validation errors |
| `action` | `string` | Form action (e.g., `'?/create'`) |
| `submitLabel` | `string?` | Button text |
Renders a `<form>` with inputs determined by `fieldTypes` and `relationOptions`. When `record` is provided, fields are pre-filled. The `form` prop carries SvelteKit action data for error display and field re-population after failed submissions.
### ComboBox
| Prop | Type | Description |
|---|---|---|
| `id` | `string` | Element ID |
| `name` | `string` | Form field name |
| `options` | `{ label; value }[]` | Dropdown items |
| `placeholder` | `string?` | Placeholder text |
| `value` | `string \| number?` | Pre-selected value |
Features: type-to-search filtering, keyboard navigation (ArrowUp/Down, Enter, Escape),
clear button, scroll-to-highlight.
### RelatedHasOne
| Prop | Type | Description |
|---|---|---|
| `label` | `string` | Section heading |
| `record` | `Record<string, unknown> \| null` | Related record |
| `displayFields` | `string[]` | Fields to display as key-value pairs |
| `editSlug` | `string?` | Links to edit the related record |
### RelatedHasMany
| Prop | Type | Description |
|---|---|---|
| `label` | `string` | Section heading (shows count) |
| `records` | `Record<string, unknown>[]` | Related records |
| `displayFields` | `string[]` | Table columns |
| `editSlug` | `string?` | Per-row edit links |
## 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` |
## 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 }; // staff only sees active products
}
}
```
+19 -19
View File
@@ -91,10 +91,10 @@ const models: AdminModelConfig[] = [
}
},
{
slug: 'affirmations',
modelName: 'Affirmation',
displayName: 'Affirmations',
displayNameSingular: 'Affirmation',
slug: 'proclamations',
modelName: 'Proclamation',
displayName: 'Proclamations',
displayNameSingular: 'Proclamation',
idType: 'String',
include: { solfeggioFrequency: true, category: { include: { parent: true } } },
listFields: ['name', 'category.name', 'solfeggioFrequency.name', 'price'],
@@ -115,7 +115,7 @@ const models: AdminModelConfig[] = [
valueField: 'id'
},
categoryId: {
model: 'AffirmationCategory',
model: 'ProclamationCategory',
labelField: 'name',
valueField: 'id'
}
@@ -127,7 +127,7 @@ const models: AdminModelConfig[] = [
displayName: 'Solfeggio Frequencies',
displayNameSingular: 'Solfeggio Frequency',
idType: 'Int',
include: { affirmations: true },
include: { proclamations: true },
listFields: ['name', 'value'],
searchFields: ['name'],
formFields: ['name', 'value'],
@@ -138,21 +138,21 @@ const models: AdminModelConfig[] = [
},
relatedRecords: [
{
relationKey: 'affirmations',
label: 'Affirmations',
relationKey: 'proclamations',
label: 'Proclamations',
type: 'has-many',
displayFields: ['name', 'price'],
editSlug: 'affirmations'
editSlug: 'proclamations'
}
]
},
{
slug: 'affirmation-categories',
modelName: 'AffirmationCategory',
displayName: 'Affirmation Categories',
displayNameSingular: 'Affirmation Category',
slug: 'proclamation-categories',
modelName: 'ProclamationCategory',
displayName: 'Proclamation Categories',
displayNameSingular: 'Proclamation Category',
idType: 'Int',
include: { affirmations: true, children: true, parent: true },
include: { proclamations: true, children: true, parent: true },
listFields: ['name', 'parent.name'],
searchFields: ['name'],
formFields: ['name', 'parentId'],
@@ -162,22 +162,22 @@ const models: AdminModelConfig[] = [
parentId: 'Int'
},
relationOptions: {
parentId: { model: 'AffirmationCategory', labelField: 'name', valueField: 'id' }
parentId: { model: 'ProclamationCategory', labelField: 'name', valueField: 'id' }
},
relatedRecords: [
{
relationKey: 'affirmations',
label: 'Affirmations',
relationKey: 'proclamations',
label: 'Proclamations',
type: 'has-many',
displayFields: ['name', 'price'],
editSlug: 'affirmations'
editSlug: 'proclamations'
},
{
relationKey: 'children',
label: 'Sub-categories',
type: 'has-many',
displayFields: ['name'],
editSlug: 'affirmation-categories'
editSlug: 'proclamation-categories'
}
]
},
+1 -1
View File
@@ -6,7 +6,7 @@ export const ROLES = {
} as const;
export function hasRole(userRoleId: number | null | undefined, requiredRoleId: number): boolean {
if (userRoleId == null) return false;
if (userRoleId == null || userRoleId < 1) return false;
return userRoleId <= requiredRoleId;
}
+82
View File
@@ -54,3 +54,85 @@ export function mockUserWithProfile(overrides?: Record<string, unknown>) {
...overrides
});
}
/** User with profile that includes addresses. */
export function mockUserWithProfileAndAddresses(overrides?: Record<string, unknown>) {
return mockUser({
profile: {
id: 'profile-1',
userId: 'user-1',
firstName: 'Jane',
lastName: 'Doe',
phoneNumber: '5550100',
addresses: [
{
id: 'addr-1',
profileId: 'profile-1',
street: '123 Main St',
city: 'Dallas',
state: 'TX',
postalCode: '75001',
country: 'US'
}
]
},
...overrides
});
}
/** Minimal license key object. */
export function mockLicenseKey(overrides?: Record<string, unknown>) {
return {
id: 'key-1',
key: 'AAAA-BBBB-CCCC-DDDD',
active: true,
level: 'standard',
userId: 'user-1',
...overrides
};
}
/** Minimal proclamation object with relations. */
export function mockProclamation(overrides?: Record<string, unknown>) {
return {
id: 'proc-1',
name: 'Test Proclamation',
content: '<p>Test content</p>',
solfeggioFrequencyId: 1,
categoryId: 1,
price: 0.99,
solfeggioFrequency: {
id: 1,
name: 'UT',
value: 396
},
category: {
id: 1,
name: 'Healing',
parentId: null,
parent: null
},
...overrides
};
}
/** Minimal proclamation category. */
export function mockCategory(overrides?: Record<string, unknown>) {
return {
id: 1,
name: 'Healing',
parentId: null,
...overrides
};
}
/** Build a URL object for use with PageServerLoad. */
export function createUrl(path: string, params?: Record<string, string>): URL {
const url = new URL(path, 'http://localhost');
if (params) {
for (const [k, v] of Object.entries(params)) {
url.searchParams.set(k, v);
}
}
return url;
}