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:
NeilClack
2026-09-10 09:38:01 -05:00
parent f4e736c92d
commit 91d162c746
127 changed files with 7182 additions and 9785 deletions
+3
View File
@@ -7,3 +7,6 @@ bun.lockb
# Miscellaneous # Miscellaneous
/static/ /static/
# Generated code
/src/lib/generated/
+30 -33
View File
@@ -9,11 +9,11 @@ AGENTS.md
at your project root: at your project root:
````text ````text
# Rhemawave Portal AI Guide # Radiant Wave Portal AI Guide
## Mission ## Mission
You are working on portal.Rhemawavetech.com. You are working on portal.radiantwavetech.com.
Act as a senior engineer making complete, production-ready changes. Act as a senior engineer making complete, production-ready changes.
@@ -97,9 +97,9 @@ Use narrower commands when appropriate.
If validation fails: If validation fails:
* Diagnose the failure. - Diagnose the failure.
* Fix the issue. - Fix the issue.
* Run validation again. - Run validation again.
Never finish with known failures. Never finish with known failures.
@@ -109,19 +109,19 @@ Never finish with known failures.
Application: Application:
portal.Rhemawavetech.com portal.radiantwavetech.com
Stack: Stack:
* SvelteKit 2 - SvelteKit 2
* Svelte 5 - Svelte 5
* TypeScript - TypeScript
* Tailwind CSS v4 - Tailwind CSS v4
* Prisma 7 - Prisma 7
* PostgreSQL - PostgreSQL
* Better Auth - Better Auth
* Vitest - Vitest
* Playwright - Playwright
Package manager: Package manager:
@@ -266,9 +266,9 @@ AdminPage.svelte
Registry metadata exists for: Registry metadata exists for:
* columns - columns
* searches - searches
* relations - relations
Forms are manually implemented per route. Forms are manually implemented per route.
@@ -309,8 +309,8 @@ Only load that file when modifying Svelte components or Svelte logic.
Testing uses: Testing uses:
* Vitest - Vitest
* Playwright browser testing - Playwright browser testing
For testing patterns, read: For testing patterns, read:
@@ -338,10 +338,10 @@ Only load that file when working on API functionality.
Prettier rules: Prettier rules:
* tabs - tabs
* single quotes - single quotes
* no trailing commas - no trailing commas
* 80 character print width - 80 character print width
Follow existing formatting. Follow existing formatting.
@@ -391,17 +391,14 @@ Confirm:
2. Relevant tests/checks were run. 2. Relevant tests/checks were run.
3. Failures were fixed. 3. Failures were fixed.
4. The final response explains: 4. The final response explains:
- what changed
* what changed - files modified
* files modified - verification performed
* verification performed
Do not stop at: Do not stop at:
* "I found the issue" - "I found the issue"
* "Here is a plan" - "Here is a plan"
* "The next step would be" - "The next step would be"
Finish the work. Finish the work.
+2 -2
View File
@@ -1,4 +1,4 @@
# portal.Rhemawavetech.com # portal.radiantwavetech.com
SvelteKit 2 + Svelte 5 (runes) + Tailwind CSS v4 + Prisma 7 + PostgreSQL + Better-Auth. SvelteKit 2 + Svelte 5 (runes) + Tailwind CSS v4 + Prisma 7 + PostgreSQL + Better-Auth.
@@ -332,7 +332,7 @@ The `$effect` resets `dirty = false` on mount (when the record prop changes).
| Template ID | Variables | Subject (API-level) | | Template ID | Variables | Subject (API-level) |
| ------------------------------ | --------------------------------------------------------------- | ------------------------------ | | ------------------------------ | --------------------------------------------------------------- | ------------------------------ |
| `automated-user-registration` | `firstName`, `tempPassword`, `verifyUrl`, `loginUrl` | "Your account is ready" | | `automated-user-registration` | `firstName`, `tempPassword`, `verifyUrl`, `loginUrl` | "Your account is ready" |
| `license-key-delivery` | `firstName`, `license_key` | "Your Rhemawave License Key" | | `license-key-delivery` | `firstName`, `license_key` | "Your Radiant Wave License Key" |
| `email-verification-email` | `target_email_address`, `verification_link` | "Verify your email address" | | `email-verification-email` | `target_email_address`, `verification_link` | "Verify your email address" |
Variables must be declared in the Resend template editor's Variables section. Undeclared variables render as literal `{{{varName}}}` text. Variables must be declared in the Resend template editor's Variables section. Undeclared variables render as literal `{{{varName}}}` text.
+1 -1
View File
@@ -1,4 +1,4 @@
# Rhema Wave Technologies - User Portal # Radiant Wave Technologies - User Portal
A combination application that combines the main Radiant Wave API with a user-facing account management application, A combination application that combines the main Radiant Wave API with a user-facing account management application,
and an admin-facing platform management application. An all-in-one management platform for Radiant Wave and Radiant Wave Technologies. and an admin-facing platform management application. An all-in-one management platform for Radiant Wave and Radiant Wave Technologies.
+184
View File
@@ -0,0 +1,184 @@
# Admin Overhaul & Bug Fix Plan
Status: **IN PROGRESS** — checkboxes are ticked as work is completed and committed.
## Context
The admin section is mid-refactor. The generic `$lib/svelte-admin/*` CRUD layer and
`AdminTable.svelte` were deleted; the **Profiles** page was rebuilt by hand with
`DataTable.svelte`, `AdminModal.svelte`, and route-specific form component(s). The
remaining admin pages still import the deleted modules, so the app does not compile.
This plan:
1. Fixes the reported Profiles-page bugs (stale buttons, unified User/Profile identity,
in-modal status messages, centered modal with backdrop).
2. Rebuilds every other admin page to match the Profiles page look, style, and
functionality — KISS, no clever abstractions.
3. Removes the **Admin dashboard** — the nav links are the navigation.
4. Uses `DataTable` / `AdminModal` / `Toast` as dumb containers; each page owns its
own form logic.
Roles: `SUPER = 1`, `ADMIN = 2`, `STAFF = 3`, `USER = 4`. Admin area requires `roleId <= 3`.
## Decisions Locked In
- **Dashboard**: removed. `/admin` redirects to `/admin/profiles`.
- **Profile email is the identity.** The User dropdown is removed from the Profiles
form. One Email field:
- Linked profile (`userId` set): field shows/edits the User's email; saving
syncs the change to the `User` record.
- Unlinked profile: email lives on the Profile only.
- On save, if an existing `User` matches the entered email, the profile is linked
to it automatically.
- **Users page** is where Users are edited (Name, Email, Role) — not the Profiles page.
- **Status messages live inside modals**, at the top, not in a page-level toast.
- **API Keys**: pending operator review. Applied: compile fix (remove `$lib/svelte-admin/roles`
dependency) + consistent styling. Modal-based rotation flow is deferred (see appendix).
---
## Checklist
### Phase 0 — Documentation & scaffolding
- [x] Document this plan at `docs/ADMIN_OVERHAUL.md` (this file)
- [x] Remove Admin dashboard: `/admin` redirects to `/admin/profiles`
- [x] Delete `(protected)/admin/+page.svelte`
- [x] Rewrite `(protected)/admin/+page.server.ts` as a `redirect(307, '/admin/profiles')`
### Phase 1 — Shared components
- [x] Rewrite `src/lib/components/admin/AdminModal.svelte`
- [x] Fixed overlay, centered panel (`fixed inset-0 z-50`)
- [x] Dark translucent backdrop (`bg-slate-900/50`), click/`Esc` to close
- [x] Panel: `max-w-2xl`, `max-h-[90vh]`, scrollable body
- [x] Header: title, Delete (when `canDelete && recordId`), close `×`
- [x] Extend `src/lib/components/DataTable.svelte`
- [x] Optional `copyableFields` → copy button per matching cell (`stopPropagation`)
- [x] Optional `draggable` + `onReorder` → drag-handle reorder column
- [x] Restyle `src/lib/components/admin/Toast.svelte` (fixed, dark chip, countdown/dismiss)
### Phase 2 — Profiles page (reference implementation)
- [x] `+page.server.ts`
- [x] Remove user-options relation data / `updateUser` action
- [x] New schema: `email` (required), `firstName`, `lastName`, `phoneNumber`
- [x] Upsert: create links to matching `User` by email; update syncs linked `User.email`;
guard `P2002` with friendly message
- [x] `ProfileFields.svelte`
- [x] Single Email field (bound from `user.email ?? record.email`); remove User select
- [x] `CreateProfileModal.svelte`
- [x] Fix skip-first `lastForm` effect bug
- [x] In-modal success banner ("Profile created") then auto-close
- [x] In-modal error banner + field errors
- [x] `EditProfileModal.svelte`
- [x] Fix skip-first `lastForm` effect bug
- [x] Reset sub-state per record (addresses, toggles, errors) on record change
- [x] Remove the "Edit User" / "Save User" section
- [x] In-modal success/error banners; keep open on successful edit
- [x] Clear & close Add-Address form on success; append returned address live
- [x] `+page.svelte`
- [x] Email column shows `user.email ?? email`; remove `users` prop plumbing
- [x] `{#key editRecord?.id}` so the edit modal remounts per record
- [x] Remove page-level `Toast` for modal feedback
- [x] New spec: `profiles/+page.server.spec.ts`
- [x] Create links by matched email; unlinked stores email only
- [x] Update syncs linked `User.email`; rejects email owned by another user
- [x] Validation failures and `P2002` handling
### Phase 3 — Users page
- [x] `+page.server.ts` — load users + role options (role-filtered for non-super);
`upsert` name/email/roleId; `delete`; `P2002` guard
- [x] `+page.svelte` — `DataTable` (Name, Email, Role, Created) + edit `AdminModal`
(Name, Email, Role select, Delete)
- [x] Replace `UserForm.svelte` usage (or convert to inline form) — no `svelte-admin`
### Phase 4 — User Roles page
- [x] `+page.server.ts` — direct `prisma.userRole` load/upsert/delete (P2003 guard)
- [x] `+page.svelte` — `DataTable` (Id, Name) + modal
### Phase 5 — Solfeggio Frequencies page
- [x] `+page.server.ts` — direct `prisma.solfeggioFrequency` load/upsert/delete (P2003 guard)
- [x] `+page.svelte` — `DataTable` (Name, Value) + modal
### Phase 6 — Proclamation Categories page
- [x] `+page.server.ts` — direct `prisma.proclamationCategory` load/upsert/delete/reorder
(P2002/P2003 guards, maxSort for new rows)
- [x] `+page.svelte` — draggable `DataTable` + modal (Name), reorder via fetch + invalidateAll
### Phase 7 — Proclamation Groups page
- [x] `+page.server.ts` — direct `prisma.proclamationGroup` load/upsert/delete/reorder
(P2002/P2003 guards)
- [x] `+page.svelte` — draggable `DataTable` (Name, Sort) + modal
### Phase 8 — License Keys page
- [x] `+page.server.ts` — direct `prisma.licenseKey` load (with user) + `createKey` + `replace`
- [x] `+page.svelte` — `DataTable` with copyable Key column; New-by-email modal; row → Replace modal
- [x] In-modal success/error banners for both flows
### Phase 9 — Proclamations page
- [x] `+page.server.ts` — direct load (strip content for non-super) + upsert (Super only,
P2002) + delete (Super only, P2003); category/group/frequency option data
- [x] `+page.svelte` — `DataTable` (Name, Category, Group, Frequency) + modal with
TipTap content editor for Super users
- [x] Update `page.server.spec.ts` and `page.svelte.spec.ts` (drop `svelte-admin` mocks)
### Phase 10 — API Keys page (operator page)
- [x] `+page.server.ts` — remove `ROLES` import (`$lib/svelte-admin/roles` deleted); use literal
- [x] `+page.svelte` — consistent styling; keep single-page rotate flow (modal conversion deferred)
- [x] Document deferred modal conversion in appendix
### Phase 11 — Tests & validation
- [x] Update `license-keys/page.server.spec.ts` (drop `svelte-admin` mocks)
- [x] `npm run check`
- [x] `npm run lint` (pre-existing `no-explicit-any` spec debt remains — see Appendix C)
- [x] `npm run test`
- [x] `npm run build` (needs env vars exported during `vite build`)
- [x] Final commit; mark every checkbox complete
---
## Appendix A — Profiles bug root causes
1. **Skip-first `lastForm` bug.** Both modals guard against the initial `null` form
with `if (lastForm === null) { lastForm = form; return; }`, which also swallows the
_first real submit result_ — the first "Add Address" save creates the DB row but the
UI never reacts (no close, no toast, no appended row). Fix: `if (!form || form === lastForm) return;`.
2. **Stale modal state across records.** `{#if editRecord}` keeps the modal mounted
between different rows, while the `seeded` flag never resets — the previous row's
addresses/user data leak in, and `record` stays stale (the reported
"my name didn't update"). Fix: `{#key editRecord?.id}` remount + per-record reseed.
3. **User data is only a snapshot.** The modal rendered `record.user.name / email`
directly. With the unified email model this display goes away; the form fields are
controlled by local `$state` and the server keeps Profile + User in sync.
## Appendix B — API Keys decision note
The API Keys page is an operator page (key rotation), not a CRUD table. It does not use
`DataTable`. Recommended future change: present the rotation confirmation and the new
key in shared `AdminModal` panels with in-modal status. That conversion is **deferred**
pending review. What was done now: compile fix (replaced the deleted `ROLES` import)
and consistent styling (simple slate card layout, indigo action buttons, red emergency
button) so the page compiles and matches the site visually.
## Appendix C — Validation caveats
1. **`npm run check`** required renaming `src/routes/(protected)/admin/profiles/+page.server.spec.ts`
to `page.server.spec.ts`: SvelteKit reserves files whose names start with `+`. The `+`-prefix
broke `svelte-kit sync` and thus the whole `check`/`build` pipeline. Renamed.
2. **`npm run lint`** still reports 142 `no-explicit-any` errors across 14 pre-existing spec files
(user/account, dashboard, license-keys, login/register/reset, etc.). These predate this overhaul
and are untouched here; fixing them is a separate cleanup.
3. **`npm run build`** needs `BETTER_AUTH_SECRET`/`BETTER_AUTH_URL`/`DATABASE_URL` exported in the
shell, because `src/lib/auth.ts` reads `$env/dynamic/private` (empty at `vite build` time) and
Better Auth rejects the default secret. Export the vars before building.
+2 -4
View File
@@ -89,8 +89,8 @@ POST:
Payload: Payload:
{ {
"license_key": "...", "license_key": "...",
"email_address": "..." "email_address": "..."
} }
Requires API key. Requires API key.
@@ -153,7 +153,6 @@ Variables:
- verifyUrl - verifyUrl
- loginUrl - loginUrl
license-key-delivery license-key-delivery
Variables: Variables:
@@ -161,7 +160,6 @@ Variables:
- firstName - firstName
- license_key - license_key
email-verification-email email-verification-email
Variables: Variables:
+13 -12
View File
@@ -36,13 +36,13 @@ Always destructure $props() with defaults.
Preferred pattern: Preferred pattern:
let { let {
record = {} as Record<string, unknown>, record = {} as Record<string, unknown>,
fieldErrors = {} as Record<string, string>, fieldErrors = {} as Record<string, string>,
isSuper = false isSuper = false
}: { }: {
record?: Record<string, unknown>; record?: Record<string, unknown>;
fieldErrors?: Record<string, string>; fieldErrors?: Record<string, string>;
isSuper?: boolean; isSuper?: boolean;
} = $props(); } = $props();
Do not assume props exist. Do not assume props exist.
@@ -66,7 +66,7 @@ Correct:
let name = $state(''); let name = $state('');
$effect(() => { $effect(() => {
name = String(record.name ?? ''); name = String(record.name ?? '');
}); });
Use $effect whenever component state needs to follow changing props. Use $effect whenever component state needs to follow changing props.
@@ -88,11 +88,11 @@ Example:
let dirty = $state(false); let dirty = $state(false);
$effect(() => { $effect(() => {
dirty = false; dirty = false;
}); });
$effect(() => { $effect(() => {
name = String(record.name ?? ''); name = String(record.name ?? '');
}); });
This ensures a new record starts clean. This ensures a new record starts clean.
@@ -116,9 +116,10 @@ This can fail to update correctly.
Use: Use:
<select onchange={(e) => frequency = e.currentTarget.value}> <select onchange={(e) => frequency = e.currentTarget.value}>
<option value="1" selected={frequency === '1'}>
One <option value="1" selected={frequency === '1'}>
</option> One
</option>
</select> </select>
Use selected on options. Use selected on options.
+29 -29
View File
@@ -30,13 +30,13 @@ Tests are colocated with source files.
Client tests: Client tests:
*.svelte.test.ts _.svelte.test.ts
*.svelte.spec.ts _.svelte.spec.ts
Server tests: Server tests:
*.test.ts _.test.ts
*.spec.ts _.spec.ts
--- ---
@@ -79,7 +79,7 @@ Node.
Always reset mocks: Always reset mocks:
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
}); });
--- ---
@@ -109,15 +109,15 @@ Mock only the methods used.
Example: Example:
vi.mock('$lib/db', () => ({ vi.mock('$lib/db', () => ({
prisma: { prisma: {
proclamation: { proclamation: {
findMany: vi.fn(), findMany: vi.fn(),
findUnique: vi.fn(), findUnique: vi.fn(),
create: vi.fn(), create: vi.fn(),
update: vi.fn(), update: vi.fn(),
delete: vi.fn() delete: vi.fn()
} }
} }
})); }));
Do not create massive unused mocks. Do not create massive unused mocks.
@@ -143,7 +143,7 @@ createLocals(mockUser() as any)
Specific roles: Specific roles:
createLocals( createLocals(
mockUser({ roleId: 4 }) as any mockUser({ roleId: 4 }) as any
) )
--- ---
@@ -172,15 +172,15 @@ Always use exact regex names.
Preferred: Preferred:
page.getByRole( page.getByRole(
'textbox', 'textbox',
{ name: /^Name$/ } { name: /^Name$/ }
) )
Avoid: Avoid:
page.getByRole( page.getByRole(
'textbox', 'textbox',
{ name: 'Name' } { name: 'Name' }
) )
because substring matching can create strict-mode failures. because substring matching can create strict-mode failures.
@@ -192,15 +192,15 @@ because substring matching can create strict-mode failures.
Because selects use selected options instead of bind:value: Because selects use selected options instead of bind:value:
const element = const element =
await page await page
.getByRole( .getByRole(
'combobox', 'combobox',
{ name: /^Frequency$/ } { name: /^Frequency$/ }
) )
.element(); .element();
expect( expect(
(element as HTMLSelectElement).value (element as HTMLSelectElement).value
).toBe('2'); ).toBe('2');
--- ---
@@ -213,9 +213,9 @@ Use:
vi.mock('$lib/components/example.svelte', () => ({ vi.mock('$lib/components/example.svelte', () => ({
default: ($anchor: Element) => { default: ($anchor: Element) => {
const div = document.createElement('div'); const div = document.createElement('div');
$anchor.replaceWith(div); $anchor.replaceWith(div);
} }
})); }));
Imports using mocked modules must come after vi.mock(). Imports using mocked modules must come after vi.mock().
+330
View File
@@ -0,0 +1,330 @@
# API Updates — Category Flattening, Groups, Subscription Level Removal
**Release:** v2 schema change (migration `20260821000000_flat_categories_groups_drop_subscription_levels`)
**Status:** Breaking changes. Clients must be updated.
## Summary of changes
| Endpoint | Change |
| ----------------------------------------- | ------------------------------------------------------------------------------------ |
| `GET /api/v2/categories` | **Breaking**: `parentId` removed from every record. Categories are now a flat list. |
| `GET /api/v2/groups` | **New**: returns the group list (former sub-categories). |
| `GET /api/v2/proclamations` | **Breaking**: `groupId` added; `subscriptionLevelId` removed. |
| `GET /api/v2/subscription-levels` | **Removed**: subscription levels no longer exist. |
| `POST /api/v2/license` | **Breaking**: response no longer contains `subscription_level`. |
| `POST /api/v2/automations/create/account` | **Breaking**: `subscription_level` is no longer accepted or required in the payload. |
| `POST /api/v1/license` | **Breaking**: response no longer contains `subscription_level`. |
| `POST /api/v1/automations/create/account` | **Breaking**: `subscription_level` is no longer accepted or required in the payload. |
| `GET /api/v1/categories` | Payload implicitly changed: rows no longer contain `parentId`. |
### Data model mapping
- Former **parent** categories (`Spiritual`, `Physical`, `Mental`, `Emotional`, `Financial`,
`Social`, `Biblical`, ...) became the flat **Category** list.
- Former **child** sub-categories (`General`, `Specific`, `Precise`, `Prayers`, `Scripture`)
became **Groups**.
- A proclamation's former child category becomes its `group`; its former parent category becomes
its `category`. A proclamation may move between categories without changing its group.
- Proclamations that had no sub-category (e.g. under `Custom (Client Specific)`) were assigned to
the `General` group.
- Every proclamation now has exactly one category and one group.
---
## OpenAPI 3.0 specification
```yaml
openapi: 3.0.3
info:
title: Radiant Wave Portal API
version: 2.1.0
description: |
Changes in this release:
* ProclamationCategory hierarchy flattened; parentId removed everywhere.
* New ProclamationGroup resource replaces sub-categories as a movable filter.
* SubscriptionLevel resource and all level-based gating removed.
paths:
/api/v2/categories:
get:
summary: List categories (flat)
description: >
Returns all proclamation categories as a flat, ordered list.
BREAKING: the previous version returned a hierarchical list where each
record carried a nullable parentId linking children to parents. That
field no longer exists.
tags: [Categories]
responses:
'200':
description: Flat category list.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Category'
examples:
newResponse:
summary: New response
value:
- id: 4
name: Spiritual
sortOrder: 0
- id: 2
name: Physical
sortOrder: 1
oldResponseDeprecated:
summary: Old (removed) response shape
value:
- id: 5
name: General
sortOrder: 0
parentId: 4
- id: 9
name: Specific
sortOrder: 1
parentId: 4
/api/v2/groups:
get:
summary: List groups
description: >
NEW ENDPOINT. Returns all proclamation groups ordered by sortOrder.
Groups replace the former sub-categories and act as a movable filter:
a proclamation can be moved from one category to another while keeping
its group.
tags: [Groups]
responses:
'200':
description: Group list.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Group'
example:
- id: 1
name: General
sortOrder: 0
- id: 2
name: Specific
sortOrder: 1
/api/v2/proclamations:
get:
summary: List proclamations
description: >
Returns proclamations with their canonical frequency id and optional
color palette. BREAKING: each record now carries groupId (required,
always present) and no longer carries subscriptionLevelId. When the
optional entitlement headers are supplied, only the caller's entitled
proclamations are returned; there is no longer any per-level gating.
tags: [Proclamations]
parameters:
- in: header
name: x-license-key
required: false
schema: { type: string }
description: License key. Must be paired with x-email.
- in: header
name: x-email
required: false
schema: { type: string }
description: Account email. Must be paired with x-license-key.
responses:
'200':
description: Proclamation list.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Proclamation'
examples:
newResponse:
summary: New response
value:
- id: b40fdcb9-703c-420e-af32-a081964c6f08
name: Peace & Relaxation
content: <p>The text content</p>
categoryId: 4
groupId: 1
solfeggioFrequencyId: 5
colorPalette:
[
'#32CD32',
'#3CB371',
'#90EE90',
'#98FB98',
'#ADFFA9',
'#C8FFD2',
'#F0FFF0'
]
oldResponseDeprecated:
summary: Old (removed) response shape
value:
- id: b40fdcb9-703c-420e-af32-a081964c6f08
name: Peace & Relaxation
content: <p>The text content</p>
categoryId: 5
solfeggioFrequencyId: 5
subscriptionLevelId: 1
'400':
description: Entitlement headers incomplete or invalid.
/api/v2/subscription-levels:
get:
summary: REMOVED
deprecated: true
description: >
This endpoint was removed. Subscription levels no longer exist in the
data model; clients must stop calling it.
tags: [Removed]
responses:
'404':
description: Route no longer exists.
/api/v2/license:
post:
summary: Validate a license key
description: >
Validates a key/email pair. BREAKING: the response no longer contains a
subscription_level object.
tags: [License]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [license_key, email_address]
properties:
license_key: { type: string, example: ABCD-EFGH-IJKL-MNOP }
email_address: { type: string, format: email }
responses:
'200':
description: Key is valid.
content:
application/json:
schema:
$ref: '#/components/schemas/LicenseResponse'
examples:
newResponse:
summary: New response
value:
key: ABCD-EFGH-IJKL-MNOP
email_address: user@example.com
oldResponseDeprecated:
summary: Old (removed) response shape
value:
key: ABCD-EFGH-IJKL-MNOP
email_address: user@example.com
subscription_level:
id: 2
name: Advanced
description: General + Specific
'400':
description: Validation failure (missing fields, unknown key, wrong owner).
/api/v2/automations/create/account:
post:
summary: Create account + license key (GoHighLevel automation)
description: >
Creates a portal account when needed and issues a license key.
BREAKING: subscription_level was removed from the request contract. The
field is neither required nor validated anymore; sending it is ignored.
Signature headers (x-timestamp, x-nonce, x-signature) are unchanged.
tags: [Automations]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email]
properties:
email: { type: string, format: email }
name: { type: string }
responses:
'200':
description: Account and license key created.
'400':
description: Missing/invalid email.
'401':
description: Invalid signature.
components:
schemas:
Category:
type: object
required: [id, name, sortOrder]
properties:
id: { type: integer }
name: { type: string }
sortOrder: { type: integer }
Group:
type: object
required: [id, name, sortOrder]
properties:
id: { type: integer }
name: { type: string }
sortOrder: { type: integer }
Proclamation:
type: object
required: [id, name, content, categoryId, groupId, solfeggioFrequencyId]
properties:
id:
type: string
format: uuid
name: { type: string }
content: { type: string, description: HTML formatted content. }
categoryId:
type: integer
description: Flat category this proclamation belongs to.
groupId:
type: integer
description: >
Group membership (movable filter). Independent of categoryId;
changing the category does not change the group.
solfeggioFrequencyId:
type: integer
minimum: 1
maximum: 9
description: Canonical kiosk frequency id (not the DB autoincrement id).
colorPalette:
type: array
items: { type: string }
minItems: 7
maxItems: 7
description: Present only when a palette exists for the frequency.
LicenseResponse:
type: object
required: [key, email_address]
properties:
key: { type: string }
email_address: { type: string, format: email }
```
---
## Client migration checklist
1. **Kiosk / API consumers**
- Stop reading `parentId`; treat `/api/v2/categories` as flat.
- Fetch `/api/v2/groups` once at startup for the filter list.
- Read `groupId` from each proclamation record for filtering.
- Remove all references to `subscriptionLevelId` and `/api/v2/subscription-levels`.
- Remove `subscription_level` handling from license validation responses.
2. **GoHighLevel automation**
- Remove `subscription_level` from the account-creation webhook payload
(optional but recommended; the field is now ignored).
3. **Admin portal**
- Manage categories under Admin → Proclamation Categories (flat list, drag to reorder).
- Manage groups under Admin → Proclamation Groups (drag to reorder).
- Proclamations now select one Category and one Group independently.
+2 -2
View File
@@ -1,11 +1,11 @@
{ {
"name": "portal.Rhemawavetech.com", "name": "portal.radiantwavetech.com",
"version": "0.0.1", "version": "0.0.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "portal.Rhemawavetech.com", "name": "portal.radiantwavetech.com",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@prisma/adapter-pg": "^7.8.0", "@prisma/adapter-pg": "^7.8.0",
+2 -2
View File
@@ -1,5 +1,5 @@
{ {
"name": "portal.Rhemawavetech.com", "name": "portal.radiantwavetech.com",
"private": true, "private": true,
"version": "0.0.1", "version": "0.0.1",
"type": "module", "type": "module",
@@ -67,4 +67,4 @@
"sveltekit-superforms": "^2.30.2", "sveltekit-superforms": "^2.30.2",
"zod": "^4.4.3" "zod": "^4.4.3"
} }
} }
-756
View File
@@ -1,756 +0,0 @@
# RadiantWave Portal API v2 Schema
This document defines the **required** API surface for the RadiantWave kiosk
application and serves as the implementation contract for the portal
(`../portal`).
It supersedes `payloads.md` and `ApiUpdates.md`, which described a v1 API that
does not meet the application's requirements.
---
## Background and constraints
- The kiosk application (`this repo`) talks to
`https://portal.radiantwavetech.com/api/v1`.
- The **existing v1 routes must not change**: previously shipped kiosk binaries
depend on them. The v1 routes and their exact request/response shapes stay
frozen.
- To bring the API into compliance with this application's expectations we
introduce a **new, versioned API under `/api/v2`**, and migrate the kiosk
client to call `/api/v2` instead of `/api/v1`.
- The kiosk client currently uses a single base URL
(`src/network/api_client.h` → `API_BASE_URL`). All endpoint paths are
relative to that base, so migrating the base URL is the only client-side
change required.
Compliance means: every endpoint the kiosk parses must return exactly the
fields the kiosk expects, in the shapes documented below. Deviations break
provisioning, content sync, or rendering.
---
## Versioning strategy
| Route space | Status | Consumer |
|-------------|---------------------------|-------------------------------------------------------|
| `/api/v1/*` | Frozen, never modified | Old/shipped kiosk binaries, GoHighLevel automation |
| `/api/v2/*` | New (this document) | Current kiosk application (after base-URL migration) |
Rules:
1. Never change the behavior or response shape of an existing `/api/v1/*`
route. If a v1 fix is required, fix it in v2.
2. New capabilities ship under `/api/v2` only.
3. A consumer may pin to the major version it was built against. The kiosk
pins to `/api/v2`.
---
## General conventions (v2)
- Base URL: `https://portal.radiantwavetech.com/api/v2`.
- All routes require HTTPS (TLS 1.2+; TLS 1.3 recommended).
- Success responses are JSON with `content-type: application/json`.
- Error bodies use the shape `{"message": "..."}` for route-handler errors and
`{"error": "..."}` for the authentication layer (see Security).
- Any response code outside the documented success code is a failure; the
kiosk surfaces `message`/`error` text when present.
- API keys are sent as `Authorization: Bearer sk_<hex>` and are **required on
every v2 route** except the signed onboarding route (below).
- Credentials (license keys, emails) must **never** be passed in the query
string. v1 accepted `?email=&key=`; v2 must not.
- Request body sizes are capped (see Security); oversized bodies are rejected
with `413`.
---
## Canonical data model: solfeggio frequencies
The kiosk uses a fixed 9-preset frequency scheme. Frequency ids must be
**canonical** — they appear on proclamations and color palettes and must match
the kiosk's own `frequencyIdForHz()` mapping:
| Canonical id | Hz | Frequency (kiosk label) |
|--------------|------|------------------------------|
| 1 | 174 | Grounding / Relief (285 Hz is an alias of id 1) |
| 2 | 396 | Root |
| 3 | 417 | Sacral |
| 4 | 432 | Clarity / Wellbeing |
| 5 | 528 | Solar Plexus (fallback palette) |
| 6 | 639 | Heart |
| 7 | 741 | Throat |
| 8 | 852 | 3rd Eye |
| 9 | 963 | Crown |
**Compliance gap in v1:** the portal's `SolfeggioFrequency` table stores 7
rows whose ids do **not** match this scheme (id 1 = 936 Hz, id 5 = 528 Hz,
id 7 = 396 Hz, etc.). Because proclamations and color palettes are keyed by
frequency id, returning portal ids would make the kiosk look up the wrong
palette (e.g. a 396 Hz proclamation would render with the 741 Hz palette).
**v2 requirement:** every `solfeggioFrequencyId` emitted by v2 must be a
canonical id (1–9) whose `value` resolves through the kiosk map above.
Frequencies that have no canonical id (e.g. 936 Hz) must not be referenced by
any v2 proclamation or palette. See Implementation notes for the reconciling
strategy.
---
## Endpoints
### 1. `GET /api/v2/frequencies`
Lists the canonical solfeggio frequencies.
Request:
```
GET /api/v2/frequencies
Authorization: Bearer sk_<hex>
```
Response (200):
```json
[
{ "id": 1, "name": "Grounding / Relief", "value": 174 },
{ "id": 2, "name": "Root", "value": 396 },
{ "id": 3, "name": "Sacral", "value": 417 },
{ "id": 4, "name": "Clarity / Wellbeing", "value": 432 },
{ "id": 5, "name": "Solar Plexus", "value": 528 },
{ "id": 6, "name": "Heart", "value": 639 },
{ "id": 7, "name": "Throat", "value": 741 },
{ "id": 8, "name": "3rd Eye", "value": 852 },
{ "id": 9, "name": "Crown", "value": 963 }
]
```
Contract (kiosk `parseFrequenciesJson`):
- `id`: int, 1–9, **non-zero** (entries with id 0 are skipped).
- `name`: string.
- `value`: int, Hz. Should be a value the kiosk's `frequencyIdForHz()` map
recognizes (174, 285, 396, 417, 432, 528, 639, 741, 852, 963).
Errors:
| Code | Body | Meaning |
|------|------|---------|
| 200 | array of frequency objects | Success |
| 401 | `{"error": "..."}` | Missing/invalid API key |
---
### 2. `GET /api/v2/categories`
Lists all proclamation categories (identical shape to v1).
Request:
```
GET /api/v2/categories
Authorization: Bearer sk_<hex>
```
Response (200):
```json
[
{ "id": 4, "name": "Spiritual", "sortOrder": 0, "parentId": null },
{ "id": 5, "name": "General", "sortOrder": 0, "parentId": 4 }
]
```
Contract (kiosk `parseCategoriesJson`):
- `id`: int, **non-zero** (id 0 entries are skipped).
- `name`: string.
- `parentId`: int for children, **`null` for root** categories (kiosk maps
null → 0).
- `sortOrder`: int.
Errors:
| Code | Body | Meaning |
|------|------|---------|
| 200 | array of category objects | Success |
| 401 | `{"error": "..."}` | Missing/invalid API key |
---
### 3. `GET /api/v2/proclamations`
Returns the proclamation library. This is the kiosk's primary content sync.
Request (default: full library):
```
GET /api/v2/proclamations
Authorization: Bearer sk_<hex>
```
Optional entitlement filter (full records, see below):
```
GET /api/v2/proclamations
Authorization: Bearer sk_<hex>
X-License-Key: XXXX-XXXX-XXXX-XXXX
X-Email: user@example.com
```
> Header-based credentials replace the v1 `?email=&key=` query form. Do not
> read credentials from the URL.
Response (200) — every item is a **full proclamation record**:
```json
[
{
"id": "b40fdcb9-703c-420e-af32-a081964c6f08",
"name": "Peace & Relaxation",
"content": "<p>The text content, HTML formatted</p>",
"categoryId": 5,
"solfeggioFrequencyId": 5,
"subscriptionLevelId": 1,
"colorPalette": ["#32CD32", "#3CB371", "#90EE90", "#98FB98", "#ADFFA9", "#C8FFD2", "#F0FFF0"]
}
]
```
Contract (kiosk `parseProclamationsJson` + `parseProclamationPalettesJson`):
- `id`: string UUID, **required non-empty** (empty ids are skipped).
- `name`: string, used as the display title. Kiosk strips HTML from it.
- `content`: string, **HTML** (the kiosk strips tags and unescapes entities
before rendering). Plain text is also acceptable but HTML is the convention.
- `categoryId`: int, maps to a `/categories` id.
- `solfeggioFrequencyId`: int, **must be a canonical id (1–9)** — see the
canonical model above.
- `subscriptionLevelId`: int or `null`. The kiosk treats missing/null/≤0 as
level 1 (Basic).
- `colorPalette` (optional): array of exactly 7 `"#RRGGBB"` strings, indexed
color 0 (primary) through color 6 (tertiary). When present, the kiosk can
color a proclamation directly from this payload (first proclamation per
frequency wins for palette derivation). When absent, the kiosk falls back to
`/color-palettes`, then to its own 528 Hz seed.
With entitlement headers (`X-License-Key` + `X-Email`):
- Validate the key exists, is active, and belongs to the email's user.
- Return the **full proclamation records** the user may access (matching the
record shape above), **not** raw `UserProclamations` join-table rows.
(v1 returned join rows, which the kiosk parser cannot consume — this is the
primary v1 compliance defect this endpoint fixes.)
Errors:
| Code | Body | Meaning |
|------|------|---------|
| 200 | array of proclamation records | Success |
| 400 | `{"message": "not-authorized"}` | Entitlement headers supplied but key or email does not exist |
| 400 | `{"message": "this key doesn't appear to belong to you"}` | Key exists but owned by a different user |
| 401 | `{"error": "..."}` | Missing/invalid API key |
---
### 4. `POST /api/v2/license`
Validates a license key / email pair and returns the full subscription level.
Request:
```
POST /api/v2/license
Content-Type: application/json
Authorization: Bearer sk_<hex>
```
```json
{
"license_key": "XXXX-XXXX-XXXX-XXXX",
"email_address": "user@example.com"
}
```
Response (200):
```json
{
"key": "XXXX-XXXX-XXXX-XXXX",
"email_address": "user@example.com",
"subscription_level": {
"id": 2,
"name": "Advanced",
"description": "General + Specific"
}
}
```
`subscription_level` is **always an object** `{ id, name, description }`. If
the license's level name does not match any `SubscriptionLevel` row, fall back
to a structurally consistent object:
```json
{
"key": "XXXX-XXXX-XXXX-XXXX",
"email_address": "user@example.com",
"subscription_level": { "id": null, "name": "SomeCustomLevel", "description": null }
}
```
Contract (kiosk `parseLicenseJson`):
- `key`: string, **required non-empty**.
- `email_address`: string.
- `subscription_level.name`: string, **required** for a successful validation
(the kiosk persists id/name/description into its local `system_kv` table and
uses the level for proclamation gating and the Home footer).
Errors:
| Code | Body | Meaning |
|------|------|---------|
| 200 | success object | License valid and owned by the email |
| 400 | `{"message": "license_key is required"}` | Missing `license_key` |
| 400 | `{"message": "email_address is required"}` | Missing `email_address` |
| 400 | `{"message": "license key not found"}` | No key with that value |
| 400 | `{"message": "license key is not active"}` | Key exists but inactive |
| 400 | `{"message": "user does not exist"}` | No user with that email |
| 400 | `{"message": "key does not belong to specified user"}` | Key owned by a different user |
| 401 | `{"error": "..."}` | Missing/invalid API key |
> The kiosk client also tolerates the v1 string form and flat
> `subscription_level_id` / `subscription_level_description` fields. v2 should
> always send the object form; the fallbacks exist only for transitional
> compatibility.
---
### 5. `GET /api/v2/subscription-levels`
Lists the subscription levels.
Request:
```
GET /api/v2/subscription-levels
Authorization: Bearer sk_<hex>
```
Response (200):
```json
[
{ "id": 1, "name": "Basic", "description": "General only" },
{ "id": 2, "name": "Advanced", "description": "General + Specific" },
{ "id": 3, "name": "Elite", "description": "General + Specific + Precise" }
]
```
Errors:
| Code | Body | Meaning |
|------|------|---------|
| 200 | array of `{id, name, description}` | Success |
| 401 | `{"error": "..."}` | Missing/invalid API key |
---
### 6. `GET /api/v2/color-palettes`
Exposes the 7-color palette for every canonical frequency so the portal admin
can update colors without a kiosk code release.
Request:
```
GET /api/v2/color-palettes
Authorization: Bearer sk_<hex>
```
Response (200) — one entry per canonical frequency (1–9):
```json
[
{
"solfeggioFrequencyId": 1,
"colors": ["#8B6914", "#B8860B", "#DAA520", "#E8B64C", "#FFD700", "#FFE9A8", "#FFF4CC"]
},
{
"solfeggioFrequencyId": 5,
"colors": ["#32CD32", "#3CB371", "#90EE90", "#98FB98", "#ADFFA9", "#C8FFD2", "#F0FFF0"]
}
]
```
Contract (kiosk `parseColorPalettesJson`):
- `solfeggioFrequencyId`: int, **non-zero**, canonical id.
- `colors`: array of `"#RRGGBB"` (or `"RRGGBB"`) strings, exactly 7.
Kiosk fallback: `db::getPaletteForFrequencyId()` falls back to the 528 Hz
palette for any frequency id with no rows; the startup seed backfills missing
ids with `INSERT OR IGNORE`, so portal values win after a successful sync.
Errors:
| Code | Body | Meaning |
|------|------|---------|
| 200 | array of `{solfeggioFrequencyId, colors}` | Success |
| 401 | `{"error": "..."}` | Missing/invalid API key |
---
### 7. `POST /api/v2/automations/create/account`
Creates (or reuses) a user account and issues a brand-new active license key.
The key is **not returned** — it is emailed to the user. This is the only v2
route that does not require a bearer API key; instead it requires an
**HMAC-SHA256 signature** (see Security). It is used for first-run provisioning
and by the GoHighLevel automation.
Request:
```
POST /api/v2/automations/create/account
Content-Type: application/json
X-Timestamp: <unix seconds>
X-Nonce: <random uuid>
X-Signature: <hex HMAC-SHA256 of canonical request with the onboarding secret>
```
```json
{
"email": "user@example.com",
"name": "Optional Display Name",
"subscription_level": "Basic"
}
```
- `email` (required, string).
- `name` (optional, string; defaults to the local part of `email`).
- `subscription_level` (required, string) — one of `Basic`, `Advanced`,
`Elite` (must match a `SubscriptionLevel.name`).
Response (200):
```json
{
"success": true,
"message": "Account and license key created",
"user_id": "uuid"
}
```
Errors:
| Code | Body | Meaning |
|------|------|---------|
| 200 | success object | Account (+ license key) created |
| 400 | `{"message": "email is required"}` | Missing/empty `email` |
| 400 | `{"message": "subscription_level is required"}` | Missing/empty `subscription_level` |
| 400 | `{"message": "subscription_level \"<name>\" not found"}` | Unknown level name |
| 401 | `{"error": "invalid signature"}` | Bad/expired/replayed HMAC signature |
| 429 | `{"message": "rate limit exceeded"}` | Too many requests from this IP/email |
| 500 | `{"message": "Failed to create account"}` | User creation failed |
| 500 | `{"message": "Failed to find or create user"}` | User could not be located |
Behavior (unchanged from v1):
- If the email already belongs to a user, no new user is created — only a new
license key for that user.
- A new user receives a temporary-password/verify email and a license-key
email. Sending up to two emails, so this endpoint must be rate-limited
aggressively.
---
## Compliance contract (kiosk → v2)
| Kiosk consumer | v2 endpoint | Required fields / shape |
|----------------|-------------|-------------------------|
| `sync::syncProclamationsFromApi` (Home + ProclamationOptions sync) | `GET /proclamations`, `GET /categories` | Full proclamation records; `id`, `name`, `content`, `categoryId`, `solfeggioFrequencyId` (canonical), `subscriptionLevelId` |
| `license::validateStoredLicense` (License Key provisioning page) | `POST /license` | `subscription_level` as `{id, name, description}` object |
| `api::fetchColorPalettes` / `sync::syncColorPalettesFromApi` (readiness, to be wired) | `GET /color-palettes` | `[{solfeggioFrequencyId, colors[7]}]` |
| `sync::syncProclamationPalettesFromApi` (readiness, to be wired) | `GET /proclamations` | Embedded `colorPalette` array per proclamation |
| `api::fetchFrequencies` (readiness) | `GET /frequencies` | Canonical id 1–9 + `value` in Hz |
| Future level-picker UI | `GET /subscription-levels` | `{id, name, description}` |
| Provisioning / GHL automation | `POST /automations/create/account` | `email`, `subscription_level`, optional `name` |
---
## Implementation instructions (portal, `../portal`)
### Route layout
Create the v2 route tree alongside the frozen v1 tree:
```
src/routes/api/v2/
frequencies/+server.ts
categories/+server.ts
proclamations/+server.ts
license/+server.ts
subscription-levels/+server.ts
color-palettes/+server.ts
automations/create/account/+server.ts
```
### Auth gate (`src/hooks.server.ts`)
Extend the existing handler so v2 auth is **always enforced** (do not honor
`API_KEYS_ACTIVE` for v2):
```ts
const isV2 = event.url.pathname.startsWith('/api/v2/');
const isSignedOnboarding =
event.url.pathname.startsWith('/api/v2/automations/');
if (isV2 && !isSignedOnboarding) {
// validateApiKey() ALWAYS runs for v2 — no env-var bypass.
const authHeader = event.request.headers.get('authorization');
if (!authHeader?.startsWith('Bearer ')) {
return new Response(JSON.stringify({ error: 'API key required' }), {
status: 401,
headers: { 'content-type': 'application/json' }
});
}
const record = await validateApiKey(authHeader.slice(7));
if (!record) {
return new Response(JSON.stringify({ error: 'Invalid or expired API key' }), {
status: 401,
headers: { 'content-type': 'application/json' }
});
}
return resolve(event);
}
```
Keep the existing v1 branch untouched.
### Shared serializer (`src/lib/server/v2-serializers.ts`)
Add one module that maps DB rows to the v2 wire format. The critical piece is
frequency-id reconciliation:
```ts
// Canonical id <- Hz (kiosk frequencyIdForHz map).
const CANONICAL_ID_FOR_HZ: Record<number, number> = {
174: 1, 285: 1, 396: 2, 417: 3, 432: 4,
528: 5, 639: 6, 741: 7, 852: 8, 963: 9
};
function canonicalIdForFrequency(freq: { id: number; value: number; name: string }): number | null {
const id = CANONICAL_ID_FOR_HZ[freq.value];
return id ?? null; // e.g. 936 Hz has no canonical id -> excluded from v2
}
export function serializeProclamation(p: Proclamation & { solfeggioFrequency: { value: number } }) {
const solfeggioFrequencyId = CANONICAL_ID_FOR_HZ[p.solfeggioFrequency.value];
if (!solfeggioFrequencyId) return null; // drop proclamations with no canonical frequency
return {
id: p.id,
name: p.name,
content: p.content,
categoryId: p.categoryId,
solfeggioFrequencyId,
subscriptionLevelId: p.subscriptionLevelId,
// optional; read from the palette source below
colorPalette: paletteFor(p.solfeggioFrequency.value) // 7x "#RRGGBB" or omit
};
}
```
Key points:
- Derive the emitted `solfeggioFrequencyId` from the joined frequency's
**Hz value**, never from the DB id. This keeps v1 rows (and their ids)
untouched while v2 emits canonical ids.
- Proclamations whose frequency has no canonical id (e.g. 936 Hz) are excluded
from v2 responses (or re-assigned a canonical frequency in data).
- Add the missing frequencies to the DB so content can be authored for them:
`174, 285, 432, 963` (by `value`). Do not delete or renumber the existing
rows that v1 depends on.
### Color palettes
Add a portal-editable palette source keyed by canonical frequency, for example
a `colorPalette` JSON column on `SolfeggioFrequency` or a
`ColorPalette`/`ColorPaletteColor` table:
```prisma
model ColorPalette {
solfeggioFrequencyId Int @id
colors Json // ["#RRGGBB", ... x7]
}
```
- Seed all 9 canonical palettes to match the kiosk's current seed
(`src/db/database_manager.cpp` → `seedColorPalettes()`).
- `GET /color-palettes` returns one entry per canonical id.
- `GET /proclamations` embeds the palette for the proclamation's canonical
frequency.
### License endpoint
In `license/+server.ts`, resolve the level object like v1 change #1 suggested:
```ts
const level = await prisma.subscriptionLevel.findFirst({
where: { name: storedKey.level }
});
subscription_level = level
? { id: level.id, name: level.name, description: level.description }
: { id: null, name: storedKey.level, description: null };
```
### Proclamations entitlement filter
When `X-License-Key` / `X-Email` headers are present, validate them exactly as
v1 does, but return **full proclamation records** (the user's entitled set via
`UserProclamations` + `Proclamation` include), never join-table rows.
### Tests
- Unit-test the serializers with fixtures matching the kiosk's offline tests
in `tests/test_api_client.cpp` (string vs object `subscription_level`,
`subscriptionLevelId: null`, `parentId: null`, 7-color palettes, canonical
frequency mapping incl. 936 → dropped).
- Live smoke tests against `/api/v2` mirroring the existing v1 live tests.
---
## Security: restricting v2 to the kiosk application
These endpoints should be reachable only from enrolled kiosk devices. v1's
auth could be disabled with `API_KEYS_ACTIVE=false`; v2 must not offer that
bypass. Suggestions below are ordered by impact.
### 1. Mandatory bearer-key auth on every content route
- Every v2 route except the signed onboarding route requires
`Authorization: Bearer sk_<hex>`.
- Enforce it in `hooks.server.ts` unconditionally (no env toggle).
- Keys are generated with `generateApiKey()`, stored **SHA-256 hashed**, and
only the `sk_xxxxxxxxxx` prefix is ever displayed. Never log or return the
plaintext key.
- Use constant-time comparison / lookup by hash (already the case via
`validateApiKey`).
### 2. Per-device credentials (enroll each kiosk)
One shared API key is a single point of failure. Instead:
- Add a `Device` record (id, serial, model, owner, enrolledAt, apiKeyId) and
issue **one unique key per kiosk** at first boot (or at manufacturing).
- Bind the key to the device: include the device id in a custom header
(`X-Device-Id`) and verify the key belongs to that device.
- A compromised key is then revoked per-device without a global rotation, and
the portal can tell which device made each request.
### 3. Least privilege / scoped keys
- Kiosk keys are read-only for content (`/frequencies`, `/categories`,
`/proclamations`, `/color-palettes`, `/subscription-levels`) and may POST to
`/license`.
- Reject any other verb/route combination with 403. Admin and data-mutation
keys are issued separately to portal staff only.
### 4. Rate limiting per key and per IP
- Content GETs: ~60/min per key/IP.
- `/license`: ~10/min per key/IP.
- `/automations/create/account`: ~5/min per IP and ~3/day per email.
- Return `429` with `Retry-After`; the kiosk client should back off and retry.
- Slippery handling for repeated failures (rate-limit on failures too).
### 5. Replay protection / request signing
- The bearer key alone is replayable if captured. Add to the high-risk routes
(and the onboarding route) an HMAC-SHA256 signature over the canonical
request (method, path, `X-Timestamp`, `X-Nonce`, raw body) using a secret
known only to the device and the server.
- Reject when `|now − X-Timestamp| > 300s` or when a nonce has been seen
before. This defeats capture-and-replay attacks over the network.
- Implement the same scheme for the onboarding route using a shared secret
configured on the portal and the GoHighLevel automation (replacing the
current entirely unauthenticated `/automations/*` exemption).
### 6. Never put credentials in URLs
- v1 accepted license keys and emails in the query string, which land in
access logs, proxies, and referrers. v2 reads credentials from the
`Authorization` header and JSON body only.
- Redact `Authorization` values, license keys, and emails in application and
proxy logs.
### 7. Client fingerprinting and bot rejection
- The kiosk sends `User-Agent: RadiantWave-Kiosk/0.1`. Require it (optionally
with a signed `X-Client-Id`) and return `403` for generic browsers, `curl`,
and headless scanners.
- Reject request bodies that are not `application/json` where JSON is
expected; enforce request-size limits (reject with `413`).
- At the proxy layer: require TLS 1.2+, prefer 1.3, set HSTS; consider
restricting v2 to expected deployment geographies and blocking TOR/exit
nodes and known hosting/bot ASNs.
### 8. Input validation and uniform errors
- Validate each body against a strict schema: email ≤ 254 chars and a valid
shape, `license_key` must match `^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$`,
`subscription_level` must equal a known level name, `name` ≤ 120 chars.
- Reject embedded HTML/script in string fields.
- Return uniform `{"message": ...}` bodies; never leak stack traces or SQL
errors. Avoid verbose error differences that enable credential enumeration
(see below).
### 9. License / onboarding abuse resistance
- `/license` success/failure is enumerable by design (a fixed key format). At
minimum rate-limit aggressively. Consider returning a generic
`{"message": "invalid credentials"}` (possibly plus a structured `code`
field for the kiosk) so attackers cannot distinguish "user exists" from
"key exists". Keep the specific messages only behind authenticated admin
tooling if they are needed for support.
- `/automations/create/account` mints real license keys and sends email, so
protect it hardest: HMAC signature (above), per-IP and per-email rate
limits, an email-domain/typo check, and a honeypot field. Never expose it
without signature as an unauthenticated "create account" funnel.
### 10. Monitoring, logging, alerting
- Log every v2 request as structured data: timestamp, endpoint, method, status,
key prefix, `X-Device-Id`, IP, latency. Never log full keys, emails, or
bodies.
- Alert on: error-rate spikes, repeated 401s, unexpected `/license` volume,
signup bursts, single-device request floods, and revoked-key usage.
- Automatically suspend a device that shows abusive patterns; require
re-enrollment.
### 11. Key lifecycle
- Rotate keys on a schedule; support the existing `rotateApiKey` 48-hour grace
and emergency revocation per device.
- Auto-expire keys that have not authenticated in N days.
- Consider an authenticated `/api/v2/auth/refresh` (or device re-enrollment)
so a kiosk can obtain a fresh key, turning a leaked key into a self-healing
problem.
---
## Rollout checklist
1. Implement the v2 routes and serializers; add unit + live smoke tests.
2. Ship v2 alongside v1 (both live; v1 frozen).
3. Flip the kiosk base URL: `src/network/api_client.h` →
`https://portal.radiantwavetech.com/api/v2` (already done by this change).
4. Run the kiosk live tests with `RW_LIVE_API_KEY` set:
- `./build/test_api_client`
- `./build/test_license_service`
5. Update portal-admin color palettes and verify a fresh kiosk sync picks them
up (now wired via `/color-palettes`).
6. Keep `/api/v1` online until all deployed kiosks are on the v2 build;
monitor v1 traffic and sunset it only after the fleet is migrated.
@@ -0,0 +1,115 @@
-- Flatten ProclamationCategory hierarchy into a flat list and introduce
-- ProclamationGroup (formerly child sub-categories).
--
-- This migration is intentionally NON-DESTRUCTIVE:
-- * ProclamationGroup / Proclamation.groupId are added additively.
-- * Former child categories are ARCHIVED to ProclamationCategoryLegacy
-- before the hierarchy (parentId) is removed, so no category data is lost.
-- * Subscription-level resources (SubscriptionLevel,
-- ProclamationCategorySubscriptionLevel, Proclamation.subscriptionLevelId,
-- LicenseKey.level) are LEFT IN PLACE as unmanaged legacy data. They are no
-- longer modeled by the Prisma schema and are ignored by the application,
-- but their data is preserved and no DROP is attempted on tables the deploy
-- role may not own.
-- CreateTable
CREATE TABLE "ProclamationGroup" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "ProclamationGroup_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "ProclamationGroup_name_key" ON "ProclamationGroup"("name");
-- Seed groups from every distinct former sub-category name
INSERT INTO "ProclamationGroup" ("name", "sortOrder")
SELECT "name",
ROW_NUMBER() OVER (ORDER BY "name") - 1
FROM (
SELECT DISTINCT "name" FROM "ProclamationCategory" WHERE "parentId" IS NOT NULL
) AS d;
-- AddColumn (nullable during backfill)
ALTER TABLE "Proclamation" ADD COLUMN "groupId" INTEGER;
-- Backfill: former child categories become the proclamation's group; the
-- parent becomes its flat category.
UPDATE "Proclamation" AS p
SET "groupId" = g."id",
"categoryId" = c."parentId"
FROM "ProclamationCategory" AS c
JOIN "ProclamationGroup" AS g ON g."name" = c."name"
WHERE p."categoryId" = c."id" AND c."parentId" IS NOT NULL;
-- Backfill: proclamations that had no group (e.g. 'Custom (Client Specific)')
-- default to the General group, if it exists.
UPDATE "Proclamation"
SET "groupId" = (SELECT "id" FROM "ProclamationGroup" WHERE "name" = 'General')
WHERE "groupId" IS NULL;
-- Safety net: any proclamation still unmapped takes the first group.
UPDATE "Proclamation"
SET "groupId" = (SELECT "id" FROM "ProclamationGroup" ORDER BY "sortOrder" LIMIT 1)
WHERE "groupId" IS NULL;
ALTER TABLE "Proclamation" ALTER COLUMN "groupId" SET NOT NULL;
-- AddForeignKey
ALTER TABLE "Proclamation" ADD CONSTRAINT "Proclamation_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "ProclamationGroup"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- Archive former child categories before removing them, so their data is
-- preserved and recoverable. The legacy table also records each child's
-- parent for audit/recovery.
CREATE TABLE "ProclamationCategoryLegacy" (
"id" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"parentId" INTEGER,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "ProclamationCategoryLegacy_pkey" PRIMARY KEY ("id")
);
INSERT INTO "ProclamationCategoryLegacy" ("id", "name", "parentId", "sortOrder")
SELECT "id", "name", "parentId", "sortOrder"
FROM "ProclamationCategory"
WHERE "parentId" IS NOT NULL;
-- Archive category->subscription mappings for the child categories being
-- removed, so the cascade delete below does not destroy this (deprecated)
-- mapping data. Requires read access to the join table, but never ownership.
CREATE TABLE "ProclamationCategorySubscriptionLevelLegacy" (
"categoryId" INTEGER NOT NULL,
"subscriptionLevelId" INTEGER NOT NULL,
CONSTRAINT "ProclamationCategorySubscriptionLevelLegacy_pkey" PRIMARY KEY ("categoryId", "subscriptionLevelId")
);
INSERT INTO "ProclamationCategorySubscriptionLevelLegacy" ("categoryId", "subscriptionLevelId")
SELECT j."categoryId", j."subscriptionLevelId"
FROM "ProclamationCategorySubscriptionLevel" AS j
JOIN "ProclamationCategory" AS c ON c."id" = j."categoryId" AND c."parentId" IS NOT NULL;
-- Remove former child categories (their data is archived above; the
-- subscription-level join rows for them are archived above and the
-- SubscriptionLevel table itself is left untouched).
DELETE FROM "ProclamationCategory" WHERE "parentId" IS NOT NULL;
-- Drop self-relation
ALTER TABLE "ProclamationCategory" DROP CONSTRAINT "ProclamationCategory_parentId_fkey";
-- DropIndex
DROP INDEX "ProclamationCategory_parentId_name_key";
-- AlterTable
ALTER TABLE "ProclamationCategory" DROP COLUMN "parentId";
-- CreateIndex
CREATE UNIQUE INDEX "ProclamationCategory_name_key" ON "ProclamationCategory"("name");
-- Subscription-level resources are intentionally left in place (unmanaged).
-- No DROP TABLE / DROP COLUMN / DROP CONSTRAINT is performed here, so this
-- migration never requires ownership of pre-existing subscription tables and
-- never destroys subscription data.
@@ -0,0 +1,28 @@
-- DropForeignKey
ALTER TABLE "LicenseKey" DROP CONSTRAINT "LicenseKey_userId_fkey";
-- DropIndex
DROP INDEX "Profile_userId_key";
-- AlterTable
ALTER TABLE "LicenseKey" ADD COLUMN "email" TEXT,
ALTER COLUMN "userId" DROP NOT NULL;
-- AlterTable
ALTER TABLE "Profile" ADD COLUMN "email" TEXT,
ALTER COLUMN "userId" DROP NOT NULL;
-- CreateIndex
CREATE INDEX "LicenseKey_userId_idx" ON "LicenseKey"("userId");
-- CreateIndex
CREATE INDEX "LicenseKey_email_idx" ON "LicenseKey"("email");
-- CreateIndex
CREATE INDEX "Profile_userId_idx" ON "Profile"("userId");
-- CreateIndex
CREATE INDEX "Profile_email_idx" ON "Profile"("email");
-- AddForeignKey
ALTER TABLE "LicenseKey" ADD CONSTRAINT "LicenseKey_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+62 -45
View File
@@ -25,7 +25,7 @@ model User {
role UserRole? @relation(fields: [roleId], references: [id]) role UserRole? @relation(fields: [roleId], references: [id])
sessions Session[] sessions Session[]
accounts Account[] accounts Account[]
profile Profile? profiles Profile[]
licenseKeys LicenseKey[] licenseKeys LicenseKey[]
userProclamations UserProclamations[] userProclamations UserProclamations[]
@@ -37,28 +37,32 @@ model User {
// a bunch of time trying to figure out how to customize the default Better-Auth model. // a bunch of time trying to figure out how to customize the default Better-Auth model.
model Profile { model Profile {
id String @id @default(uuid()) id String @id @default(uuid())
userId String @unique userId String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
email String?
firstName String firstName String
lastName String lastName String
phoneNumber String phoneNumber String
ghlContactId String? @map("ghl_contact_id") ghlContactId String? @map("ghl_contact_id")
addresses Address[] addresses Address[]
@@index([userId])
@@index([email])
} }
model Address { model Address {
id String @id @default(uuid()) id String @id @default(uuid())
profileId String profileId String
profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade) profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade)
street String street String
city String city String
state String state String
postalCode String postalCode String
country String country String
type String @default("Mailing") type String @default("Mailing")
isDefaultMailing Boolean @default(false) isDefaultMailing Boolean @default(false)
isDefaultBilling Boolean @default(false) isDefaultBilling Boolean @default(false)
isDefaultShipping Boolean @default(false) isDefaultShipping Boolean @default(false)
} }
model Session { model Session {
@@ -123,9 +127,16 @@ model Proclamation {
solfeggioFrequency SolfeggioFrequency @relation(fields: [solfeggioFrequencyId], references: [id]) solfeggioFrequency SolfeggioFrequency @relation(fields: [solfeggioFrequencyId], references: [id])
categoryId Int categoryId Int
category ProclamationCategory @relation(fields: [categoryId], references: [id]) category ProclamationCategory @relation(fields: [categoryId], references: [id])
groupId Int
group ProclamationGroup @relation(fields: [groupId], references: [id])
userProclamations UserProclamations[] userProclamations UserProclamations[]
subscriptionLevelId Int // Keeping old FK for migration stability }
subscriptionLevel SubscriptionLevel @relation(fields: [subscriptionLevelId], references: [id])
model ProclamationGroup {
id Int @id @default(autoincrement())
name String @unique
sortOrder Int @default(0)
proclamations Proclamation[]
} }
model UserProclamations { model UserProclamations {
@@ -142,40 +153,54 @@ model UserProclamations {
model SolfeggioFrequency { model SolfeggioFrequency {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
name String @unique name String @unique
value Int @unique value Int @unique
proclamations Proclamation[] proclamations Proclamation[]
} }
model ProclamationCategory { model ProclamationCategory {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
name String name String @unique
sortOrder Int @default(0) sortOrder Int @default(0)
parentId Int? proclamations Proclamation[]
parent ProclamationCategory? @relation("CategoryHierarchy", fields: [parentId], references: [id])
children ProclamationCategory[] @relation("CategoryHierarchy")
proclamations Proclamation[]
subscriptionLevels ProclamationCategorySubscriptionLevel[]
@@unique([parentId, name])
} }
// Join table for ProclamationCategory -> SubscriptionLevel defaults // Archive of the former child sub-categories removed when the
model ProclamationCategorySubscriptionLevel { // ProclamationCategory hierarchy was flattened. Kept for recovery/audit.
categoryId Int // `@@ignore` keeps it out of the generated client while Prisma remains aware
category ProclamationCategory @relation(fields: [categoryId], references: [id], onDelete: Cascade) // of the table so `migrate dev` does not report it as an untracked table.
model ProclamationCategoryLegacy {
id Int @id
name String
parentId Int?
sortOrder Int @default(0)
@@map("ProclamationCategoryLegacy")
@@ignore
}
// Archive of the category->subscription-level mappings for the child
// categories removed when the category hierarchy was flattened. Kept with
// `@@ignore` so Prisma remains aware of the table but it is excluded from the
// generated client and Prisma's own migrations.
model ProclamationCategorySubscriptionLevelLegacy {
categoryId Int
subscriptionLevelId Int subscriptionLevelId Int
subscriptionLevel SubscriptionLevel @relation(fields: [subscriptionLevelId], references: [id], onDelete: Cascade)
@@id([categoryId, subscriptionLevelId]) @@id([categoryId, subscriptionLevelId])
@@map("ProclamationCategorySubscriptionLevelLegacy")
@@ignore
} }
model LicenseKey { model LicenseKey {
id String @id @default(uuid()) id String @id @default(uuid())
key String @unique key String @unique
active Boolean active Boolean
level String userId String?
userId String user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
user User @relation(fields: [userId], references: [id]) email String?
@@index([userId])
@@index([email])
} }
model ApiKey { model ApiKey {
@@ -187,14 +212,6 @@ model ApiKey {
revokedAt DateTime? revokedAt DateTime?
} }
model SubscriptionLevel {
id Int @id @default(autoincrement())
name String
description String?
proclamations Proclamation[]
categoryLevels ProclamationCategorySubscriptionLevel[]
}
// Portal-editable 7-color palette per canonical solfeggio frequency id (1-9). // Portal-editable 7-color palette per canonical solfeggio frequency id (1-9).
// `solfeggioFrequencyId` is the canonical id the kiosk uses (see // `solfeggioFrequencyId` is the canonical id the kiosk uses (see
// v2-serializers.ts), not the DB autoincrement id of `SolfeggioFrequency`. // v2-serializers.ts), not the DB autoincrement id of `SolfeggioFrequency`.
@@ -203,4 +220,4 @@ model ColorPalette {
colors Json // ["#RRGGBB", ... x7] colors Json // ["#RRGGBB", ... x7]
@@map("ColorPalette") @@map("ColorPalette")
} }
+26 -43
View File
@@ -72,43 +72,26 @@ const colorPalettes: { solfeggioFrequencyId: number; colors: string[] }[] = [
} }
]; ];
const subscriptionLevels = [ const proclamationGroups = [
{ id: 1, name: 'Basic', description: 'General only' }, { name: 'General', sortOrder: 0 },
{ id: 2, name: 'Advanced', description: 'General + Specific' }, { name: 'Specific', sortOrder: 1 },
{ id: 3, name: 'Elite', description: 'General + Specific + Precise' } { name: 'Precise', sortOrder: 2 },
{ name: 'Prayers', sortOrder: 3 },
{ name: 'Scripture', sortOrder: 4 }
]; ];
// Flat category list (formerly hierarchical parents).
const proclamationCategories = [ const proclamationCategories = [
{ id: 4, name: 'Spiritual', parentId: null }, { id: 4, name: 'Spiritual' },
{ id: 5, name: 'General', parentId: 4 }, { id: 2, name: 'Physical' },
{ id: 9, name: 'Specific', parentId: 4 }, { id: 16, name: 'Mental' },
{ id: 40, name: 'Precise', parentId: 4 }, { id: 17, name: 'Custom (Client Specific)' },
{ id: 2, name: 'Physical', parentId: null }, { id: 19, name: 'Financial' },
{ id: 3, name: 'General', parentId: 2 }, { id: 20, name: 'Emotional' },
{ id: 10, name: 'Specific', parentId: 2 }, { id: 21, name: 'Social' },
{ id: 32, name: 'Precise', parentId: 2 }, { id: 41, name: 'Biblical' },
{ id: 16, name: 'Mental', parentId: null }, { id: 42, name: 'Personal Growth & Development' },
{ id: 30, name: 'General', parentId: 16 }, { id: 46, name: 'Sports' }
{ id: 31, name: 'Specific', parentId: 16 },
{ id: 38, name: 'Precise', parentId: 16 },
{ id: 17, name: 'Custom (Client Specific)', parentId: null },
{ id: 19, name: 'Financial', parentId: null },
{ id: 28, name: 'General', parentId: 19 },
{ id: 29, name: 'Specific', parentId: 19 },
{ id: 37, name: 'Precise', parentId: 19 },
{ id: 20, name: 'Emotional', parentId: null },
{ id: 24, name: 'General', parentId: 20 },
{ id: 26, name: 'Specific', parentId: 20 },
{ id: 45, name: 'Precise', parentId: 20 },
{ id: 21, name: 'Social', parentId: null },
{ id: 22, name: 'General', parentId: 21 },
{ id: 23, name: 'Specific', parentId: 21 },
{ id: 39, name: 'Precise', parentId: 21 },
{ id: 41, name: 'Biblical', parentId: null },
{ id: 42, name: 'Personal Growth & Development', parentId: null },
{ id: 43, name: 'Prayers', parentId: 41 },
{ id: 44, name: 'Scripture', parentId: 41 },
{ id: 46, name: 'Sports', parentId: null }
]; ];
async function main() { async function main() {
@@ -130,6 +113,15 @@ async function main() {
}); });
} }
console.log('Seeding ProclamationGroup...');
for (const group of proclamationGroups) {
await prisma.proclamationGroup.upsert({
where: { name: group.name },
create: group,
update: group
});
}
console.log('Seeding ProclamationCategory...'); console.log('Seeding ProclamationCategory...');
for (const cat of proclamationCategories) { for (const cat of proclamationCategories) {
await prisma.proclamationCategory.upsert({ await prisma.proclamationCategory.upsert({
@@ -139,15 +131,6 @@ async function main() {
}); });
} }
console.log('Seeding SubscriptionLevel...');
for (const level of subscriptionLevels) {
await prisma.subscriptionLevel.upsert({
where: { id: level.id },
create: level,
update: level
});
}
console.log('Seeding ColorPalette...'); console.log('Seeding ColorPalette...');
for (const palette of colorPalettes) { for (const palette of colorPalettes) {
await prisma.colorPalette.upsert({ await prisma.colorPalette.upsert({
+2 -15
View File
@@ -3,23 +3,10 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <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% %sveltekit.head%
</head> </head>
<body class="m-0" data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">
<div class="flex h-dvh w-dvw flex-col">%sveltekit.body%</div> <div>%sveltekit.body%</div>
</body> </body>
</html> </html>
+1 -1
View File
@@ -51,6 +51,6 @@ export const auth = betterAuth({
window: 60, window: 60,
max: 100 max: 100
}, },
trustedOrigins: ['http://localhost:5173', 'https://portal.rhemawavetech.com'], trustedOrigins: ['http://localhost:5173', 'https://portal.radiantwavetech.com'],
plugins: [sveltekitCookies(getRequestEvent)] plugins: [sveltekitCookies(getRequestEvent)]
}); });
+215
View File
@@ -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 -12
View File
@@ -17,8 +17,8 @@
deleteAction?: string; deleteAction?: string;
} = $props(); } = $props();
function handleBackdrop(e: MouseEvent) { function handleBackdrop() {
if (e.target === e.currentTarget) onClose?.(); onClose?.();
} }
function handleKeydown(e: KeyboardEvent) { function handleKeydown(e: KeyboardEvent) {
@@ -44,24 +44,27 @@
{#if open} {#if open}
<div <div
role="presentation" class="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/50 p-4"
class="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/50 pt-12"
onclick={handleBackdrop}
onkeydown={handleKeydown} onkeydown={handleKeydown}
role="presentation"
> >
<div class="absolute inset-0" onclick={handleBackdrop} aria-hidden="true"></div>
<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 <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"> <div class="flex items-center gap-2">
{#if canDelete && recordId} {#if canDelete && recordId}
<button <button
type="button" type="button"
onclick={confirmDelete} 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 Delete
</button> </button>
@@ -69,12 +72,14 @@
<button <button
type="button" type="button"
onclick={onClose} onclick={onClose}
class="px-2 py-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" aria-label="Close"
>&times;</button class="rounded-md p-1.5 text-2xl leading-none text-slate-400 transition hover:bg-slate-100 hover:text-red-600"
> >
&times;
</button>
</div> </div>
</div> </div>
<div class="px-6 py-4"> <div class="overflow-y-auto px-6 py-5">
{@render children()} {@render children()}
</div> </div>
</div> </div>
-248
View File
@@ -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>
+11 -15
View File
@@ -1,11 +1,5 @@
<script lang="ts"> <script lang="ts">
let { let { message = '' }: { message?: string } = $props();
message = '',
type = 'success' as 'success' | 'error'
}: {
message?: string;
type?: 'success' | 'error';
} = $props();
let remaining = $state(5); let remaining = $state(5);
let visible = $state(true); let visible = $state(true);
@@ -32,15 +26,17 @@
{#if visible} {#if visible}
<div <div
role="alert" 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 === 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"
'success'
? 'border-green-600 bg-green-50 text-green-800'
: 'border-red-600 bg-red-50 text-red-800'}"
> >
<span class="flex-1">{message}</span> <span>{message}</span>
<span class="text-xs tabular-nums opacity-70">{remaining}s</span> <span class="text-xs text-slate-400">{remaining}s</span>
<button type="button" onclick={dismiss} class="ml-1 leading-none font-bold hover:opacity-70" <button
>&times;</button type="button"
onclick={dismiss}
aria-label="Dismiss"
class="rounded p-1 text-slate-400 transition hover:text-white"
> >
&times;
</button>
</div> </div>
{/if} {/if}
+4 -2
View File
@@ -2,6 +2,8 @@
let { children } = $props(); let { children } = $props();
</script> </script>
<div class="flex min-h-0 flex-1 items-center justify-center overflow-auto"> <div class="mx-auto w-full max-w-md px-4 py-8 sm:py-12">
{@render children()} <div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8">
{@render children()}
</div>
</div> </div>
-103
View File
@@ -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} />
+5 -10
View File
@@ -57,6 +57,11 @@ export type Verification = Prisma.VerificationModel
* *
*/ */
export type Proclamation = Prisma.ProclamationModel export type Proclamation = Prisma.ProclamationModel
/**
* Model ProclamationGroup
*
*/
export type ProclamationGroup = Prisma.ProclamationGroupModel
/** /**
* Model UserProclamations * Model UserProclamations
* *
@@ -72,11 +77,6 @@ export type SolfeggioFrequency = Prisma.SolfeggioFrequencyModel
* *
*/ */
export type ProclamationCategory = Prisma.ProclamationCategoryModel export type ProclamationCategory = Prisma.ProclamationCategoryModel
/**
* Model ProclamationCategorySubscriptionLevel
*
*/
export type ProclamationCategorySubscriptionLevel = Prisma.ProclamationCategorySubscriptionLevelModel
/** /**
* Model LicenseKey * Model LicenseKey
* *
@@ -87,11 +87,6 @@ export type LicenseKey = Prisma.LicenseKeyModel
* *
*/ */
export type ApiKey = Prisma.ApiKeyModel export type ApiKey = Prisma.ApiKeyModel
/**
* Model SubscriptionLevel
*
*/
export type SubscriptionLevel = Prisma.SubscriptionLevelModel
/** /**
* Model ColorPalette * Model ColorPalette
* *
+5 -10
View File
@@ -81,6 +81,11 @@ export type Verification = Prisma.VerificationModel
* *
*/ */
export type Proclamation = Prisma.ProclamationModel export type Proclamation = Prisma.ProclamationModel
/**
* Model ProclamationGroup
*
*/
export type ProclamationGroup = Prisma.ProclamationGroupModel
/** /**
* Model UserProclamations * Model UserProclamations
* *
@@ -96,11 +101,6 @@ export type SolfeggioFrequency = Prisma.SolfeggioFrequencyModel
* *
*/ */
export type ProclamationCategory = Prisma.ProclamationCategoryModel export type ProclamationCategory = Prisma.ProclamationCategoryModel
/**
* Model ProclamationCategorySubscriptionLevel
*
*/
export type ProclamationCategorySubscriptionLevel = Prisma.ProclamationCategorySubscriptionLevelModel
/** /**
* Model LicenseKey * Model LicenseKey
* *
@@ -111,11 +111,6 @@ export type LicenseKey = Prisma.LicenseKeyModel
* *
*/ */
export type ApiKey = Prisma.ApiKeyModel export type ApiKey = Prisma.ApiKeyModel
/**
* Model SubscriptionLevel
*
*/
export type SubscriptionLevel = Prisma.SubscriptionLevelModel
/** /**
* Model ColorPalette * Model ColorPalette
* *
File diff suppressed because one or more lines are too long
@@ -392,13 +392,12 @@ export const ModelName = {
UserRole: 'UserRole', UserRole: 'UserRole',
Verification: 'Verification', Verification: 'Verification',
Proclamation: 'Proclamation', Proclamation: 'Proclamation',
ProclamationGroup: 'ProclamationGroup',
UserProclamations: 'UserProclamations', UserProclamations: 'UserProclamations',
SolfeggioFrequency: 'SolfeggioFrequency', SolfeggioFrequency: 'SolfeggioFrequency',
ProclamationCategory: 'ProclamationCategory', ProclamationCategory: 'ProclamationCategory',
ProclamationCategorySubscriptionLevel: 'ProclamationCategorySubscriptionLevel',
LicenseKey: 'LicenseKey', LicenseKey: 'LicenseKey',
ApiKey: 'ApiKey', ApiKey: 'ApiKey',
SubscriptionLevel: 'SubscriptionLevel',
ColorPalette: 'ColorPalette' ColorPalette: 'ColorPalette'
} as const } as const
@@ -415,7 +414,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
omit: GlobalOmitOptions omit: GlobalOmitOptions
} }
meta: { 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 txIsolationLevel: TransactionIsolationLevel
} }
model: { 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: { UserProclamations: {
payload: Prisma.$UserProclamationsPayload<ExtArgs> payload: Prisma.$UserProclamationsPayload<ExtArgs>
fields: Prisma.UserProclamationsFieldRefs 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: { LicenseKey: {
payload: Prisma.$LicenseKeyPayload<ExtArgs> payload: Prisma.$LicenseKeyPayload<ExtArgs>
fields: Prisma.LicenseKeyFieldRefs 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: { ColorPalette: {
payload: Prisma.$ColorPalettePayload<ExtArgs> payload: Prisma.$ColorPalettePayload<ExtArgs>
fields: Prisma.ColorPaletteFieldRefs fields: Prisma.ColorPaletteFieldRefs
@@ -1659,6 +1584,7 @@ export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof User
export const ProfileScalarFieldEnum = { export const ProfileScalarFieldEnum = {
id: 'id', id: 'id',
userId: 'userId', userId: 'userId',
email: 'email',
firstName: 'firstName', firstName: 'firstName',
lastName: 'lastName', lastName: 'lastName',
phoneNumber: 'phoneNumber', phoneNumber: 'phoneNumber',
@@ -1744,12 +1670,21 @@ export const ProclamationScalarFieldEnum = {
content: 'content', content: 'content',
solfeggioFrequencyId: 'solfeggioFrequencyId', solfeggioFrequencyId: 'solfeggioFrequencyId',
categoryId: 'categoryId', categoryId: 'categoryId',
subscriptionLevelId: 'subscriptionLevelId' groupId: 'groupId'
} as const } as const
export type ProclamationScalarFieldEnum = (typeof ProclamationScalarFieldEnum)[keyof typeof ProclamationScalarFieldEnum] 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 = { export const UserProclamationsScalarFieldEnum = {
userId: 'userId', userId: 'userId',
proclamationId: 'proclamationId', proclamationId: 'proclamationId',
@@ -1772,27 +1707,18 @@ export type SolfeggioFrequencyScalarFieldEnum = (typeof SolfeggioFrequencyScalar
export const ProclamationCategoryScalarFieldEnum = { export const ProclamationCategoryScalarFieldEnum = {
id: 'id', id: 'id',
name: 'name', name: 'name',
sortOrder: 'sortOrder', sortOrder: 'sortOrder'
parentId: 'parentId'
} as const } as const
export type ProclamationCategoryScalarFieldEnum = (typeof ProclamationCategoryScalarFieldEnum)[keyof typeof ProclamationCategoryScalarFieldEnum] 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 = { export const LicenseKeyScalarFieldEnum = {
id: 'id', id: 'id',
key: 'key', key: 'key',
active: 'active', active: 'active',
level: 'level', userId: 'userId',
userId: 'userId' email: 'email'
} as const } as const
export type LicenseKeyScalarFieldEnum = (typeof LicenseKeyScalarFieldEnum)[keyof typeof LicenseKeyScalarFieldEnum] export type LicenseKeyScalarFieldEnum = (typeof LicenseKeyScalarFieldEnum)[keyof typeof LicenseKeyScalarFieldEnum]
@@ -1810,15 +1736,6 @@ export const ApiKeyScalarFieldEnum = {
export type ApiKeyScalarFieldEnum = (typeof ApiKeyScalarFieldEnum)[keyof typeof 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 = { export const ColorPaletteScalarFieldEnum = {
solfeggioFrequencyId: 'solfeggioFrequencyId', solfeggioFrequencyId: 'solfeggioFrequencyId',
colors: 'colors' colors: 'colors'
@@ -2067,13 +1984,12 @@ export type GlobalOmitConfig = {
userRole?: Prisma.UserRoleOmit userRole?: Prisma.UserRoleOmit
verification?: Prisma.VerificationOmit verification?: Prisma.VerificationOmit
proclamation?: Prisma.ProclamationOmit proclamation?: Prisma.ProclamationOmit
proclamationGroup?: Prisma.ProclamationGroupOmit
userProclamations?: Prisma.UserProclamationsOmit userProclamations?: Prisma.UserProclamationsOmit
solfeggioFrequency?: Prisma.SolfeggioFrequencyOmit solfeggioFrequency?: Prisma.SolfeggioFrequencyOmit
proclamationCategory?: Prisma.ProclamationCategoryOmit proclamationCategory?: Prisma.ProclamationCategoryOmit
proclamationCategorySubscriptionLevel?: Prisma.ProclamationCategorySubscriptionLevelOmit
licenseKey?: Prisma.LicenseKeyOmit licenseKey?: Prisma.LicenseKeyOmit
apiKey?: Prisma.ApiKeyOmit apiKey?: Prisma.ApiKeyOmit
subscriptionLevel?: Prisma.SubscriptionLevelOmit
colorPalette?: Prisma.ColorPaletteOmit colorPalette?: Prisma.ColorPaletteOmit
} }
@@ -59,13 +59,12 @@ export const ModelName = {
UserRole: 'UserRole', UserRole: 'UserRole',
Verification: 'Verification', Verification: 'Verification',
Proclamation: 'Proclamation', Proclamation: 'Proclamation',
ProclamationGroup: 'ProclamationGroup',
UserProclamations: 'UserProclamations', UserProclamations: 'UserProclamations',
SolfeggioFrequency: 'SolfeggioFrequency', SolfeggioFrequency: 'SolfeggioFrequency',
ProclamationCategory: 'ProclamationCategory', ProclamationCategory: 'ProclamationCategory',
ProclamationCategorySubscriptionLevel: 'ProclamationCategorySubscriptionLevel',
LicenseKey: 'LicenseKey', LicenseKey: 'LicenseKey',
ApiKey: 'ApiKey', ApiKey: 'ApiKey',
SubscriptionLevel: 'SubscriptionLevel',
ColorPalette: 'ColorPalette' ColorPalette: 'ColorPalette'
} as const } as const
@@ -102,6 +101,7 @@ export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof User
export const ProfileScalarFieldEnum = { export const ProfileScalarFieldEnum = {
id: 'id', id: 'id',
userId: 'userId', userId: 'userId',
email: 'email',
firstName: 'firstName', firstName: 'firstName',
lastName: 'lastName', lastName: 'lastName',
phoneNumber: 'phoneNumber', phoneNumber: 'phoneNumber',
@@ -187,12 +187,21 @@ export const ProclamationScalarFieldEnum = {
content: 'content', content: 'content',
solfeggioFrequencyId: 'solfeggioFrequencyId', solfeggioFrequencyId: 'solfeggioFrequencyId',
categoryId: 'categoryId', categoryId: 'categoryId',
subscriptionLevelId: 'subscriptionLevelId' groupId: 'groupId'
} as const } as const
export type ProclamationScalarFieldEnum = (typeof ProclamationScalarFieldEnum)[keyof typeof ProclamationScalarFieldEnum] 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 = { export const UserProclamationsScalarFieldEnum = {
userId: 'userId', userId: 'userId',
proclamationId: 'proclamationId', proclamationId: 'proclamationId',
@@ -215,27 +224,18 @@ export type SolfeggioFrequencyScalarFieldEnum = (typeof SolfeggioFrequencyScalar
export const ProclamationCategoryScalarFieldEnum = { export const ProclamationCategoryScalarFieldEnum = {
id: 'id', id: 'id',
name: 'name', name: 'name',
sortOrder: 'sortOrder', sortOrder: 'sortOrder'
parentId: 'parentId'
} as const } as const
export type ProclamationCategoryScalarFieldEnum = (typeof ProclamationCategoryScalarFieldEnum)[keyof typeof ProclamationCategoryScalarFieldEnum] 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 = { export const LicenseKeyScalarFieldEnum = {
id: 'id', id: 'id',
key: 'key', key: 'key',
active: 'active', active: 'active',
level: 'level', userId: 'userId',
userId: 'userId' email: 'email'
} as const } as const
export type LicenseKeyScalarFieldEnum = (typeof LicenseKeyScalarFieldEnum)[keyof typeof LicenseKeyScalarFieldEnum] export type LicenseKeyScalarFieldEnum = (typeof LicenseKeyScalarFieldEnum)[keyof typeof LicenseKeyScalarFieldEnum]
@@ -253,15 +253,6 @@ export const ApiKeyScalarFieldEnum = {
export type ApiKeyScalarFieldEnum = (typeof ApiKeyScalarFieldEnum)[keyof typeof 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 = { export const ColorPaletteScalarFieldEnum = {
solfeggioFrequencyId: 'solfeggioFrequencyId', solfeggioFrequencyId: 'solfeggioFrequencyId',
colors: 'colors' colors: 'colors'
+1 -2
View File
@@ -16,12 +16,11 @@ export type * from './models/Account.ts'
export type * from './models/UserRole.ts' export type * from './models/UserRole.ts'
export type * from './models/Verification.ts' export type * from './models/Verification.ts'
export type * from './models/Proclamation.ts' export type * from './models/Proclamation.ts'
export type * from './models/ProclamationGroup.ts'
export type * from './models/UserProclamations.ts' export type * from './models/UserProclamations.ts'
export type * from './models/SolfeggioFrequency.ts' export type * from './models/SolfeggioFrequency.ts'
export type * from './models/ProclamationCategory.ts' export type * from './models/ProclamationCategory.ts'
export type * from './models/ProclamationCategorySubscriptionLevel.ts'
export type * from './models/LicenseKey.ts' export type * from './models/LicenseKey.ts'
export type * from './models/ApiKey.ts' export type * from './models/ApiKey.ts'
export type * from './models/SubscriptionLevel.ts'
export type * from './models/ColorPalette.ts' export type * from './models/ColorPalette.ts'
export type * from './commonInputTypes.ts' export type * from './commonInputTypes.ts'
+79 -60
View File
@@ -28,24 +28,24 @@ export type LicenseKeyMinAggregateOutputType = {
id: string | null id: string | null
key: string | null key: string | null
active: boolean | null active: boolean | null
level: string | null
userId: string | null userId: string | null
email: string | null
} }
export type LicenseKeyMaxAggregateOutputType = { export type LicenseKeyMaxAggregateOutputType = {
id: string | null id: string | null
key: string | null key: string | null
active: boolean | null active: boolean | null
level: string | null
userId: string | null userId: string | null
email: string | null
} }
export type LicenseKeyCountAggregateOutputType = { export type LicenseKeyCountAggregateOutputType = {
id: number id: number
key: number key: number
active: number active: number
level: number
userId: number userId: number
email: number
_all: number _all: number
} }
@@ -54,24 +54,24 @@ export type LicenseKeyMinAggregateInputType = {
id?: true id?: true
key?: true key?: true
active?: true active?: true
level?: true
userId?: true userId?: true
email?: true
} }
export type LicenseKeyMaxAggregateInputType = { export type LicenseKeyMaxAggregateInputType = {
id?: true id?: true
key?: true key?: true
active?: true active?: true
level?: true
userId?: true userId?: true
email?: true
} }
export type LicenseKeyCountAggregateInputType = { export type LicenseKeyCountAggregateInputType = {
id?: true id?: true
key?: true key?: true
active?: true active?: true
level?: true
userId?: true userId?: true
email?: true
_all?: true _all?: true
} }
@@ -151,8 +151,8 @@ export type LicenseKeyGroupByOutputType = {
id: string id: string
key: string key: string
active: boolean active: boolean
level: string userId: string | null
userId: string email: string | null
_count: LicenseKeyCountAggregateOutputType | null _count: LicenseKeyCountAggregateOutputType | null
_min: LicenseKeyMinAggregateOutputType | null _min: LicenseKeyMinAggregateOutputType | null
_max: LicenseKeyMaxAggregateOutputType | null _max: LicenseKeyMaxAggregateOutputType | null
@@ -180,17 +180,17 @@ export type LicenseKeyWhereInput = {
id?: Prisma.StringFilter<"LicenseKey"> | string id?: Prisma.StringFilter<"LicenseKey"> | string
key?: Prisma.StringFilter<"LicenseKey"> | string key?: Prisma.StringFilter<"LicenseKey"> | string
active?: Prisma.BoolFilter<"LicenseKey"> | boolean active?: Prisma.BoolFilter<"LicenseKey"> | boolean
level?: Prisma.StringFilter<"LicenseKey"> | string userId?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
userId?: Prisma.StringFilter<"LicenseKey"> | string email?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput> user?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
} }
export type LicenseKeyOrderByWithRelationInput = { export type LicenseKeyOrderByWithRelationInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
key?: Prisma.SortOrder key?: Prisma.SortOrder
active?: Prisma.SortOrder active?: Prisma.SortOrder
level?: Prisma.SortOrder userId?: Prisma.SortOrderInput | Prisma.SortOrder
userId?: Prisma.SortOrder email?: Prisma.SortOrderInput | Prisma.SortOrder
user?: Prisma.UserOrderByWithRelationInput user?: Prisma.UserOrderByWithRelationInput
} }
@@ -201,17 +201,17 @@ export type LicenseKeyWhereUniqueInput = Prisma.AtLeast<{
OR?: Prisma.LicenseKeyWhereInput[] OR?: Prisma.LicenseKeyWhereInput[]
NOT?: Prisma.LicenseKeyWhereInput | Prisma.LicenseKeyWhereInput[] NOT?: Prisma.LicenseKeyWhereInput | Prisma.LicenseKeyWhereInput[]
active?: Prisma.BoolFilter<"LicenseKey"> | boolean active?: Prisma.BoolFilter<"LicenseKey"> | boolean
level?: Prisma.StringFilter<"LicenseKey"> | string userId?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
userId?: Prisma.StringFilter<"LicenseKey"> | string email?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput> user?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
}, "id" | "key"> }, "id" | "key">
export type LicenseKeyOrderByWithAggregationInput = { export type LicenseKeyOrderByWithAggregationInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
key?: Prisma.SortOrder key?: Prisma.SortOrder
active?: Prisma.SortOrder active?: Prisma.SortOrder
level?: Prisma.SortOrder userId?: Prisma.SortOrderInput | Prisma.SortOrder
userId?: Prisma.SortOrder email?: Prisma.SortOrderInput | Prisma.SortOrder
_count?: Prisma.LicenseKeyCountOrderByAggregateInput _count?: Prisma.LicenseKeyCountOrderByAggregateInput
_max?: Prisma.LicenseKeyMaxOrderByAggregateInput _max?: Prisma.LicenseKeyMaxOrderByAggregateInput
_min?: Prisma.LicenseKeyMinOrderByAggregateInput _min?: Prisma.LicenseKeyMinOrderByAggregateInput
@@ -224,63 +224,63 @@ export type LicenseKeyScalarWhereWithAggregatesInput = {
id?: Prisma.StringWithAggregatesFilter<"LicenseKey"> | string id?: Prisma.StringWithAggregatesFilter<"LicenseKey"> | string
key?: Prisma.StringWithAggregatesFilter<"LicenseKey"> | string key?: Prisma.StringWithAggregatesFilter<"LicenseKey"> | string
active?: Prisma.BoolWithAggregatesFilter<"LicenseKey"> | boolean active?: Prisma.BoolWithAggregatesFilter<"LicenseKey"> | boolean
level?: Prisma.StringWithAggregatesFilter<"LicenseKey"> | string userId?: Prisma.StringNullableWithAggregatesFilter<"LicenseKey"> | string | null
userId?: Prisma.StringWithAggregatesFilter<"LicenseKey"> | string email?: Prisma.StringNullableWithAggregatesFilter<"LicenseKey"> | string | null
} }
export type LicenseKeyCreateInput = { export type LicenseKeyCreateInput = {
id?: string id?: string
key: string key: string
active: boolean active: boolean
level: string email?: string | null
user: Prisma.UserCreateNestedOneWithoutLicenseKeysInput user?: Prisma.UserCreateNestedOneWithoutLicenseKeysInput
} }
export type LicenseKeyUncheckedCreateInput = { export type LicenseKeyUncheckedCreateInput = {
id?: string id?: string
key: string key: string
active: boolean active: boolean
level: string userId?: string | null
userId: string email?: string | null
} }
export type LicenseKeyUpdateInput = { export type LicenseKeyUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
user?: Prisma.UserUpdateOneRequiredWithoutLicenseKeysNestedInput user?: Prisma.UserUpdateOneWithoutLicenseKeysNestedInput
} }
export type LicenseKeyUncheckedUpdateInput = { export type LicenseKeyUncheckedUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
userId?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
} }
export type LicenseKeyCreateManyInput = { export type LicenseKeyCreateManyInput = {
id?: string id?: string
key: string key: string
active: boolean active: boolean
level: string userId?: string | null
userId: string email?: string | null
} }
export type LicenseKeyUpdateManyMutationInput = { export type LicenseKeyUpdateManyMutationInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
} }
export type LicenseKeyUncheckedUpdateManyInput = { export type LicenseKeyUncheckedUpdateManyInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
userId?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
} }
export type LicenseKeyListRelationFilter = { export type LicenseKeyListRelationFilter = {
@@ -297,24 +297,24 @@ export type LicenseKeyCountOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
key?: Prisma.SortOrder key?: Prisma.SortOrder
active?: Prisma.SortOrder active?: Prisma.SortOrder
level?: Prisma.SortOrder
userId?: Prisma.SortOrder userId?: Prisma.SortOrder
email?: Prisma.SortOrder
} }
export type LicenseKeyMaxOrderByAggregateInput = { export type LicenseKeyMaxOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
key?: Prisma.SortOrder key?: Prisma.SortOrder
active?: Prisma.SortOrder active?: Prisma.SortOrder
level?: Prisma.SortOrder
userId?: Prisma.SortOrder userId?: Prisma.SortOrder
email?: Prisma.SortOrder
} }
export type LicenseKeyMinOrderByAggregateInput = { export type LicenseKeyMinOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
key?: Prisma.SortOrder key?: Prisma.SortOrder
active?: Prisma.SortOrder active?: Prisma.SortOrder
level?: Prisma.SortOrder
userId?: Prisma.SortOrder userId?: Prisma.SortOrder
email?: Prisma.SortOrder
} }
export type LicenseKeyCreateNestedManyWithoutUserInput = { export type LicenseKeyCreateNestedManyWithoutUserInput = {
@@ -363,14 +363,14 @@ export type LicenseKeyCreateWithoutUserInput = {
id?: string id?: string
key: string key: string
active: boolean active: boolean
level: string email?: string | null
} }
export type LicenseKeyUncheckedCreateWithoutUserInput = { export type LicenseKeyUncheckedCreateWithoutUserInput = {
id?: string id?: string
key: string key: string
active: boolean active: boolean
level: string email?: string | null
} }
export type LicenseKeyCreateOrConnectWithoutUserInput = { export type LicenseKeyCreateOrConnectWithoutUserInput = {
@@ -406,36 +406,36 @@ export type LicenseKeyScalarWhereInput = {
id?: Prisma.StringFilter<"LicenseKey"> | string id?: Prisma.StringFilter<"LicenseKey"> | string
key?: Prisma.StringFilter<"LicenseKey"> | string key?: Prisma.StringFilter<"LicenseKey"> | string
active?: Prisma.BoolFilter<"LicenseKey"> | boolean active?: Prisma.BoolFilter<"LicenseKey"> | boolean
level?: Prisma.StringFilter<"LicenseKey"> | string userId?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
userId?: Prisma.StringFilter<"LicenseKey"> | string email?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
} }
export type LicenseKeyCreateManyUserInput = { export type LicenseKeyCreateManyUserInput = {
id?: string id?: string
key: string key: string
active: boolean active: boolean
level: string email?: string | null
} }
export type LicenseKeyUpdateWithoutUserInput = { export type LicenseKeyUpdateWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
} }
export type LicenseKeyUncheckedUpdateWithoutUserInput = { export type LicenseKeyUncheckedUpdateWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
} }
export type LicenseKeyUncheckedUpdateManyWithoutUserInput = { export type LicenseKeyUncheckedUpdateManyWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean 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 id?: boolean
key?: boolean key?: boolean
active?: boolean active?: boolean
level?: boolean
userId?: boolean userId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs> email?: boolean
user?: boolean | Prisma.LicenseKey$userArgs<ExtArgs>
}, ExtArgs["result"]["licenseKey"]> }, ExtArgs["result"]["licenseKey"]>
export type LicenseKeySelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ export type LicenseKeySelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean id?: boolean
key?: boolean key?: boolean
active?: boolean active?: boolean
level?: boolean
userId?: boolean userId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs> email?: boolean
user?: boolean | Prisma.LicenseKey$userArgs<ExtArgs>
}, ExtArgs["result"]["licenseKey"]> }, ExtArgs["result"]["licenseKey"]>
export type LicenseKeySelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ export type LicenseKeySelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean id?: boolean
key?: boolean key?: boolean
active?: boolean active?: boolean
level?: boolean
userId?: boolean userId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs> email?: boolean
user?: boolean | Prisma.LicenseKey$userArgs<ExtArgs>
}, ExtArgs["result"]["licenseKey"]> }, ExtArgs["result"]["licenseKey"]>
export type LicenseKeySelectScalar = { export type LicenseKeySelectScalar = {
id?: boolean id?: boolean
key?: boolean key?: boolean
active?: boolean active?: boolean
level?: boolean
userId?: 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> = { 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> = { 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> = { 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> = { export type $LicenseKeyPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "LicenseKey" name: "LicenseKey"
objects: { objects: {
user: Prisma.$UserPayload<ExtArgs> user: Prisma.$UserPayload<ExtArgs> | null
} }
scalars: runtime.Types.Extensions.GetPayloadResult<{ scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string id: string
key: string key: string
active: boolean active: boolean
level: string userId: string | null
userId: string email: string | null
}, ExtArgs["result"]["licenseKey"]> }, ExtArgs["result"]["licenseKey"]>
composites: {} 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> { 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" 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. * Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved. * @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 id: Prisma.FieldRef<"LicenseKey", 'String'>
readonly key: Prisma.FieldRef<"LicenseKey", 'String'> readonly key: Prisma.FieldRef<"LicenseKey", 'String'>
readonly active: Prisma.FieldRef<"LicenseKey", 'Boolean'> readonly active: Prisma.FieldRef<"LicenseKey", 'Boolean'>
readonly level: Prisma.FieldRef<"LicenseKey", 'String'>
readonly userId: 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 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 * LicenseKey without action
*/ */
+177 -177
View File
@@ -29,13 +29,13 @@ export type AggregateProclamation = {
export type ProclamationAvgAggregateOutputType = { export type ProclamationAvgAggregateOutputType = {
solfeggioFrequencyId: number | null solfeggioFrequencyId: number | null
categoryId: number | null categoryId: number | null
subscriptionLevelId: number | null groupId: number | null
} }
export type ProclamationSumAggregateOutputType = { export type ProclamationSumAggregateOutputType = {
solfeggioFrequencyId: number | null solfeggioFrequencyId: number | null
categoryId: number | null categoryId: number | null
subscriptionLevelId: number | null groupId: number | null
} }
export type ProclamationMinAggregateOutputType = { export type ProclamationMinAggregateOutputType = {
@@ -44,7 +44,7 @@ export type ProclamationMinAggregateOutputType = {
content: string | null content: string | null
solfeggioFrequencyId: number | null solfeggioFrequencyId: number | null
categoryId: number | null categoryId: number | null
subscriptionLevelId: number | null groupId: number | null
} }
export type ProclamationMaxAggregateOutputType = { export type ProclamationMaxAggregateOutputType = {
@@ -53,7 +53,7 @@ export type ProclamationMaxAggregateOutputType = {
content: string | null content: string | null
solfeggioFrequencyId: number | null solfeggioFrequencyId: number | null
categoryId: number | null categoryId: number | null
subscriptionLevelId: number | null groupId: number | null
} }
export type ProclamationCountAggregateOutputType = { export type ProclamationCountAggregateOutputType = {
@@ -62,7 +62,7 @@ export type ProclamationCountAggregateOutputType = {
content: number content: number
solfeggioFrequencyId: number solfeggioFrequencyId: number
categoryId: number categoryId: number
subscriptionLevelId: number groupId: number
_all: number _all: number
} }
@@ -70,13 +70,13 @@ export type ProclamationCountAggregateOutputType = {
export type ProclamationAvgAggregateInputType = { export type ProclamationAvgAggregateInputType = {
solfeggioFrequencyId?: true solfeggioFrequencyId?: true
categoryId?: true categoryId?: true
subscriptionLevelId?: true groupId?: true
} }
export type ProclamationSumAggregateInputType = { export type ProclamationSumAggregateInputType = {
solfeggioFrequencyId?: true solfeggioFrequencyId?: true
categoryId?: true categoryId?: true
subscriptionLevelId?: true groupId?: true
} }
export type ProclamationMinAggregateInputType = { export type ProclamationMinAggregateInputType = {
@@ -85,7 +85,7 @@ export type ProclamationMinAggregateInputType = {
content?: true content?: true
solfeggioFrequencyId?: true solfeggioFrequencyId?: true
categoryId?: true categoryId?: true
subscriptionLevelId?: true groupId?: true
} }
export type ProclamationMaxAggregateInputType = { export type ProclamationMaxAggregateInputType = {
@@ -94,7 +94,7 @@ export type ProclamationMaxAggregateInputType = {
content?: true content?: true
solfeggioFrequencyId?: true solfeggioFrequencyId?: true
categoryId?: true categoryId?: true
subscriptionLevelId?: true groupId?: true
} }
export type ProclamationCountAggregateInputType = { export type ProclamationCountAggregateInputType = {
@@ -103,7 +103,7 @@ export type ProclamationCountAggregateInputType = {
content?: true content?: true
solfeggioFrequencyId?: true solfeggioFrequencyId?: true
categoryId?: true categoryId?: true
subscriptionLevelId?: true groupId?: true
_all?: true _all?: true
} }
@@ -199,7 +199,7 @@ export type ProclamationGroupByOutputType = {
content: string content: string
solfeggioFrequencyId: number solfeggioFrequencyId: number
categoryId: number categoryId: number
subscriptionLevelId: number groupId: number
_count: ProclamationCountAggregateOutputType | null _count: ProclamationCountAggregateOutputType | null
_avg: ProclamationAvgAggregateOutputType | null _avg: ProclamationAvgAggregateOutputType | null
_sum: ProclamationSumAggregateOutputType | null _sum: ProclamationSumAggregateOutputType | null
@@ -231,11 +231,11 @@ export type ProclamationWhereInput = {
content?: Prisma.StringFilter<"Proclamation"> | string content?: Prisma.StringFilter<"Proclamation"> | string
solfeggioFrequencyId?: Prisma.IntFilter<"Proclamation"> | number solfeggioFrequencyId?: Prisma.IntFilter<"Proclamation"> | number
categoryId?: 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> solfeggioFrequency?: Prisma.XOR<Prisma.SolfeggioFrequencyScalarRelationFilter, Prisma.SolfeggioFrequencyWhereInput>
category?: Prisma.XOR<Prisma.ProclamationCategoryScalarRelationFilter, Prisma.ProclamationCategoryWhereInput> category?: Prisma.XOR<Prisma.ProclamationCategoryScalarRelationFilter, Prisma.ProclamationCategoryWhereInput>
group?: Prisma.XOR<Prisma.ProclamationGroupScalarRelationFilter, Prisma.ProclamationGroupWhereInput>
userProclamations?: Prisma.UserProclamationsListRelationFilter userProclamations?: Prisma.UserProclamationsListRelationFilter
subscriptionLevel?: Prisma.XOR<Prisma.SubscriptionLevelScalarRelationFilter, Prisma.SubscriptionLevelWhereInput>
} }
export type ProclamationOrderByWithRelationInput = { export type ProclamationOrderByWithRelationInput = {
@@ -244,11 +244,11 @@ export type ProclamationOrderByWithRelationInput = {
content?: Prisma.SortOrder content?: Prisma.SortOrder
solfeggioFrequencyId?: Prisma.SortOrder solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder categoryId?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder groupId?: Prisma.SortOrder
solfeggioFrequency?: Prisma.SolfeggioFrequencyOrderByWithRelationInput solfeggioFrequency?: Prisma.SolfeggioFrequencyOrderByWithRelationInput
category?: Prisma.ProclamationCategoryOrderByWithRelationInput category?: Prisma.ProclamationCategoryOrderByWithRelationInput
group?: Prisma.ProclamationGroupOrderByWithRelationInput
userProclamations?: Prisma.UserProclamationsOrderByRelationAggregateInput userProclamations?: Prisma.UserProclamationsOrderByRelationAggregateInput
subscriptionLevel?: Prisma.SubscriptionLevelOrderByWithRelationInput
} }
export type ProclamationWhereUniqueInput = Prisma.AtLeast<{ export type ProclamationWhereUniqueInput = Prisma.AtLeast<{
@@ -260,11 +260,11 @@ export type ProclamationWhereUniqueInput = Prisma.AtLeast<{
content?: Prisma.StringFilter<"Proclamation"> | string content?: Prisma.StringFilter<"Proclamation"> | string
solfeggioFrequencyId?: Prisma.IntFilter<"Proclamation"> | number solfeggioFrequencyId?: Prisma.IntFilter<"Proclamation"> | number
categoryId?: 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> solfeggioFrequency?: Prisma.XOR<Prisma.SolfeggioFrequencyScalarRelationFilter, Prisma.SolfeggioFrequencyWhereInput>
category?: Prisma.XOR<Prisma.ProclamationCategoryScalarRelationFilter, Prisma.ProclamationCategoryWhereInput> category?: Prisma.XOR<Prisma.ProclamationCategoryScalarRelationFilter, Prisma.ProclamationCategoryWhereInput>
group?: Prisma.XOR<Prisma.ProclamationGroupScalarRelationFilter, Prisma.ProclamationGroupWhereInput>
userProclamations?: Prisma.UserProclamationsListRelationFilter userProclamations?: Prisma.UserProclamationsListRelationFilter
subscriptionLevel?: Prisma.XOR<Prisma.SubscriptionLevelScalarRelationFilter, Prisma.SubscriptionLevelWhereInput>
}, "id" | "name"> }, "id" | "name">
export type ProclamationOrderByWithAggregationInput = { export type ProclamationOrderByWithAggregationInput = {
@@ -273,7 +273,7 @@ export type ProclamationOrderByWithAggregationInput = {
content?: Prisma.SortOrder content?: Prisma.SortOrder
solfeggioFrequencyId?: Prisma.SortOrder solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder categoryId?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder groupId?: Prisma.SortOrder
_count?: Prisma.ProclamationCountOrderByAggregateInput _count?: Prisma.ProclamationCountOrderByAggregateInput
_avg?: Prisma.ProclamationAvgOrderByAggregateInput _avg?: Prisma.ProclamationAvgOrderByAggregateInput
_max?: Prisma.ProclamationMaxOrderByAggregateInput _max?: Prisma.ProclamationMaxOrderByAggregateInput
@@ -290,7 +290,7 @@ export type ProclamationScalarWhereWithAggregatesInput = {
content?: Prisma.StringWithAggregatesFilter<"Proclamation"> | string content?: Prisma.StringWithAggregatesFilter<"Proclamation"> | string
solfeggioFrequencyId?: Prisma.IntWithAggregatesFilter<"Proclamation"> | number solfeggioFrequencyId?: Prisma.IntWithAggregatesFilter<"Proclamation"> | number
categoryId?: Prisma.IntWithAggregatesFilter<"Proclamation"> | number categoryId?: Prisma.IntWithAggregatesFilter<"Proclamation"> | number
subscriptionLevelId?: Prisma.IntWithAggregatesFilter<"Proclamation"> | number groupId?: Prisma.IntWithAggregatesFilter<"Proclamation"> | number
} }
export type ProclamationCreateInput = { export type ProclamationCreateInput = {
@@ -299,8 +299,8 @@ export type ProclamationCreateInput = {
content: string content: string
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
group: Prisma.ProclamationGroupCreateNestedOneWithoutProclamationsInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
subscriptionLevel: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput
} }
export type ProclamationUncheckedCreateInput = { export type ProclamationUncheckedCreateInput = {
@@ -309,7 +309,7 @@ export type ProclamationUncheckedCreateInput = {
content: string content: string
solfeggioFrequencyId: number solfeggioFrequencyId: number
categoryId: number categoryId: number
subscriptionLevelId: number groupId: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
} }
@@ -319,8 +319,8 @@ export type ProclamationUpdateInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
group?: Prisma.ProclamationGroupUpdateOneRequiredWithoutProclamationsNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput
} }
export type ProclamationUncheckedUpdateInput = { export type ProclamationUncheckedUpdateInput = {
@@ -329,7 +329,7 @@ export type ProclamationUncheckedUpdateInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number categoryId?: Prisma.IntFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number groupId?: Prisma.IntFieldUpdateOperationsInput | number
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
} }
@@ -339,7 +339,7 @@ export type ProclamationCreateManyInput = {
content: string content: string
solfeggioFrequencyId: number solfeggioFrequencyId: number
categoryId: number categoryId: number
subscriptionLevelId: number groupId: number
} }
export type ProclamationUpdateManyMutationInput = { export type ProclamationUpdateManyMutationInput = {
@@ -354,7 +354,7 @@ export type ProclamationUncheckedUpdateManyInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number categoryId?: Prisma.IntFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number groupId?: Prisma.IntFieldUpdateOperationsInput | number
} }
export type ProclamationCountOrderByAggregateInput = { export type ProclamationCountOrderByAggregateInput = {
@@ -363,13 +363,13 @@ export type ProclamationCountOrderByAggregateInput = {
content?: Prisma.SortOrder content?: Prisma.SortOrder
solfeggioFrequencyId?: Prisma.SortOrder solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder categoryId?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder groupId?: Prisma.SortOrder
} }
export type ProclamationAvgOrderByAggregateInput = { export type ProclamationAvgOrderByAggregateInput = {
solfeggioFrequencyId?: Prisma.SortOrder solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder categoryId?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder groupId?: Prisma.SortOrder
} }
export type ProclamationMaxOrderByAggregateInput = { export type ProclamationMaxOrderByAggregateInput = {
@@ -378,7 +378,7 @@ export type ProclamationMaxOrderByAggregateInput = {
content?: Prisma.SortOrder content?: Prisma.SortOrder
solfeggioFrequencyId?: Prisma.SortOrder solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder categoryId?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder groupId?: Prisma.SortOrder
} }
export type ProclamationMinOrderByAggregateInput = { export type ProclamationMinOrderByAggregateInput = {
@@ -387,18 +387,13 @@ export type ProclamationMinOrderByAggregateInput = {
content?: Prisma.SortOrder content?: Prisma.SortOrder
solfeggioFrequencyId?: Prisma.SortOrder solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder categoryId?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder groupId?: Prisma.SortOrder
} }
export type ProclamationSumOrderByAggregateInput = { export type ProclamationSumOrderByAggregateInput = {
solfeggioFrequencyId?: Prisma.SortOrder solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder categoryId?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder groupId?: Prisma.SortOrder
}
export type ProclamationScalarRelationFilter = {
is?: Prisma.ProclamationWhereInput
isNot?: Prisma.ProclamationWhereInput
} }
export type ProclamationListRelationFilter = { export type ProclamationListRelationFilter = {
@@ -411,6 +406,53 @@ export type ProclamationOrderByRelationAggregateInput = {
_count?: Prisma.SortOrder _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 = { export type ProclamationCreateNestedOneWithoutUserProclamationsInput = {
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutUserProclamationsInput, Prisma.ProclamationUncheckedCreateWithoutUserProclamationsInput> create?: Prisma.XOR<Prisma.ProclamationCreateWithoutUserProclamationsInput, Prisma.ProclamationUncheckedCreateWithoutUserProclamationsInput>
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutUserProclamationsInput connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutUserProclamationsInput
@@ -509,46 +551,60 @@ export type ProclamationUncheckedUpdateManyWithoutCategoryNestedInput = {
deleteMany?: Prisma.ProclamationScalarWhereInput | Prisma.ProclamationScalarWhereInput[] deleteMany?: Prisma.ProclamationScalarWhereInput | Prisma.ProclamationScalarWhereInput[]
} }
export type ProclamationCreateNestedManyWithoutSubscriptionLevelInput = { export type ProclamationCreateWithoutGroupInput = {
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput> | Prisma.ProclamationCreateWithoutSubscriptionLevelInput[] | Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput[] id?: string
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutSubscriptionLevelInput | Prisma.ProclamationCreateOrConnectWithoutSubscriptionLevelInput[] name: string
createMany?: Prisma.ProclamationCreateManySubscriptionLevelInputEnvelope content: string
connect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[] solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
} }
export type ProclamationUncheckedCreateNestedManyWithoutSubscriptionLevelInput = { export type ProclamationUncheckedCreateWithoutGroupInput = {
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput> | Prisma.ProclamationCreateWithoutSubscriptionLevelInput[] | Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput[] id?: string
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutSubscriptionLevelInput | Prisma.ProclamationCreateOrConnectWithoutSubscriptionLevelInput[] name: string
createMany?: Prisma.ProclamationCreateManySubscriptionLevelInputEnvelope content: string
connect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[] solfeggioFrequencyId: number
categoryId: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
} }
export type ProclamationUpdateManyWithoutSubscriptionLevelNestedInput = { export type ProclamationCreateOrConnectWithoutGroupInput = {
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput> | Prisma.ProclamationCreateWithoutSubscriptionLevelInput[] | Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput[] where: Prisma.ProclamationWhereUniqueInput
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutSubscriptionLevelInput | Prisma.ProclamationCreateOrConnectWithoutSubscriptionLevelInput[] create: Prisma.XOR<Prisma.ProclamationCreateWithoutGroupInput, Prisma.ProclamationUncheckedCreateWithoutGroupInput>
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 ProclamationUncheckedUpdateManyWithoutSubscriptionLevelNestedInput = { export type ProclamationCreateManyGroupInputEnvelope = {
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput> | Prisma.ProclamationCreateWithoutSubscriptionLevelInput[] | Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput[] data: Prisma.ProclamationCreateManyGroupInput | Prisma.ProclamationCreateManyGroupInput[]
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutSubscriptionLevelInput | Prisma.ProclamationCreateOrConnectWithoutSubscriptionLevelInput[] skipDuplicates?: boolean
upsert?: Prisma.ProclamationUpsertWithWhereUniqueWithoutSubscriptionLevelInput | Prisma.ProclamationUpsertWithWhereUniqueWithoutSubscriptionLevelInput[] }
createMany?: Prisma.ProclamationCreateManySubscriptionLevelInputEnvelope
set?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[] export type ProclamationUpsertWithWhereUniqueWithoutGroupInput = {
disconnect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[] where: Prisma.ProclamationWhereUniqueInput
delete?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[] update: Prisma.XOR<Prisma.ProclamationUpdateWithoutGroupInput, Prisma.ProclamationUncheckedUpdateWithoutGroupInput>
connect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[] create: Prisma.XOR<Prisma.ProclamationCreateWithoutGroupInput, Prisma.ProclamationUncheckedCreateWithoutGroupInput>
update?: Prisma.ProclamationUpdateWithWhereUniqueWithoutSubscriptionLevelInput | Prisma.ProclamationUpdateWithWhereUniqueWithoutSubscriptionLevelInput[] }
updateMany?: Prisma.ProclamationUpdateManyWithWhereWithoutSubscriptionLevelInput | Prisma.ProclamationUpdateManyWithWhereWithoutSubscriptionLevelInput[]
deleteMany?: Prisma.ProclamationScalarWhereInput | Prisma.ProclamationScalarWhereInput[] 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 = { export type ProclamationCreateWithoutUserProclamationsInput = {
@@ -557,7 +613,7 @@ export type ProclamationCreateWithoutUserProclamationsInput = {
content: string content: string
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
subscriptionLevel: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput group: Prisma.ProclamationGroupCreateNestedOneWithoutProclamationsInput
} }
export type ProclamationUncheckedCreateWithoutUserProclamationsInput = { export type ProclamationUncheckedCreateWithoutUserProclamationsInput = {
@@ -566,7 +622,7 @@ export type ProclamationUncheckedCreateWithoutUserProclamationsInput = {
content: string content: string
solfeggioFrequencyId: number solfeggioFrequencyId: number
categoryId: number categoryId: number
subscriptionLevelId: number groupId: number
} }
export type ProclamationCreateOrConnectWithoutUserProclamationsInput = { export type ProclamationCreateOrConnectWithoutUserProclamationsInput = {
@@ -591,7 +647,7 @@ export type ProclamationUpdateWithoutUserProclamationsInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput group?: Prisma.ProclamationGroupUpdateOneRequiredWithoutProclamationsNestedInput
} }
export type ProclamationUncheckedUpdateWithoutUserProclamationsInput = { export type ProclamationUncheckedUpdateWithoutUserProclamationsInput = {
@@ -600,7 +656,7 @@ export type ProclamationUncheckedUpdateWithoutUserProclamationsInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number categoryId?: Prisma.IntFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number groupId?: Prisma.IntFieldUpdateOperationsInput | number
} }
export type ProclamationCreateWithoutSolfeggioFrequencyInput = { export type ProclamationCreateWithoutSolfeggioFrequencyInput = {
@@ -608,8 +664,8 @@ export type ProclamationCreateWithoutSolfeggioFrequencyInput = {
name: string name: string
content: string content: string
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
group: Prisma.ProclamationGroupCreateNestedOneWithoutProclamationsInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
subscriptionLevel: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput
} }
export type ProclamationUncheckedCreateWithoutSolfeggioFrequencyInput = { export type ProclamationUncheckedCreateWithoutSolfeggioFrequencyInput = {
@@ -617,7 +673,7 @@ export type ProclamationUncheckedCreateWithoutSolfeggioFrequencyInput = {
name: string name: string
content: string content: string
categoryId: number categoryId: number
subscriptionLevelId: number groupId: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
} }
@@ -647,25 +703,13 @@ export type ProclamationUpdateManyWithWhereWithoutSolfeggioFrequencyInput = {
data: Prisma.XOR<Prisma.ProclamationUpdateManyMutationInput, Prisma.ProclamationUncheckedUpdateManyWithoutSolfeggioFrequencyInput> 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 = { export type ProclamationCreateWithoutCategoryInput = {
id?: string id?: string
name: string name: string
content: string content: string
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
group: Prisma.ProclamationGroupCreateNestedOneWithoutProclamationsInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
subscriptionLevel: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput
} }
export type ProclamationUncheckedCreateWithoutCategoryInput = { export type ProclamationUncheckedCreateWithoutCategoryInput = {
@@ -673,7 +717,7 @@ export type ProclamationUncheckedCreateWithoutCategoryInput = {
name: string name: string
content: string content: string
solfeggioFrequencyId: number solfeggioFrequencyId: number
subscriptionLevelId: number groupId: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
} }
@@ -703,48 +747,38 @@ export type ProclamationUpdateManyWithWhereWithoutCategoryInput = {
data: Prisma.XOR<Prisma.ProclamationUpdateManyMutationInput, Prisma.ProclamationUncheckedUpdateManyWithoutCategoryInput> data: Prisma.XOR<Prisma.ProclamationUpdateManyMutationInput, Prisma.ProclamationUncheckedUpdateManyWithoutCategoryInput>
} }
export type ProclamationCreateWithoutSubscriptionLevelInput = { export type ProclamationCreateManyGroupInput = {
id?: string
name: string
content: string
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
}
export type ProclamationUncheckedCreateWithoutSubscriptionLevelInput = {
id?: string id?: string
name: string name: string
content: string content: string
solfeggioFrequencyId: number solfeggioFrequencyId: number
categoryId: number categoryId: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
} }
export type ProclamationCreateOrConnectWithoutSubscriptionLevelInput = { export type ProclamationUpdateWithoutGroupInput = {
where: Prisma.ProclamationWhereUniqueInput id?: Prisma.StringFieldUpdateOperationsInput | string
create: Prisma.XOR<Prisma.ProclamationCreateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput> name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
} }
export type ProclamationCreateManySubscriptionLevelInputEnvelope = { export type ProclamationUncheckedUpdateWithoutGroupInput = {
data: Prisma.ProclamationCreateManySubscriptionLevelInput | Prisma.ProclamationCreateManySubscriptionLevelInput[] id?: Prisma.StringFieldUpdateOperationsInput | string
skipDuplicates?: boolean name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
} }
export type ProclamationUpsertWithWhereUniqueWithoutSubscriptionLevelInput = { export type ProclamationUncheckedUpdateManyWithoutGroupInput = {
where: Prisma.ProclamationWhereUniqueInput id?: Prisma.StringFieldUpdateOperationsInput | string
update: Prisma.XOR<Prisma.ProclamationUpdateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedUpdateWithoutSubscriptionLevelInput> name?: Prisma.StringFieldUpdateOperationsInput | string
create: Prisma.XOR<Prisma.ProclamationCreateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput> content?: Prisma.StringFieldUpdateOperationsInput | string
} solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
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 ProclamationCreateManySolfeggioFrequencyInput = { export type ProclamationCreateManySolfeggioFrequencyInput = {
@@ -752,7 +786,7 @@ export type ProclamationCreateManySolfeggioFrequencyInput = {
name: string name: string
content: string content: string
categoryId: number categoryId: number
subscriptionLevelId: number groupId: number
} }
export type ProclamationUpdateWithoutSolfeggioFrequencyInput = { export type ProclamationUpdateWithoutSolfeggioFrequencyInput = {
@@ -760,8 +794,8 @@ export type ProclamationUpdateWithoutSolfeggioFrequencyInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string content?: Prisma.StringFieldUpdateOperationsInput | string
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
group?: Prisma.ProclamationGroupUpdateOneRequiredWithoutProclamationsNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput
} }
export type ProclamationUncheckedUpdateWithoutSolfeggioFrequencyInput = { export type ProclamationUncheckedUpdateWithoutSolfeggioFrequencyInput = {
@@ -769,7 +803,7 @@ export type ProclamationUncheckedUpdateWithoutSolfeggioFrequencyInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string content?: Prisma.StringFieldUpdateOperationsInput | string
categoryId?: Prisma.IntFieldUpdateOperationsInput | number categoryId?: Prisma.IntFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number groupId?: Prisma.IntFieldUpdateOperationsInput | number
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
} }
@@ -778,7 +812,7 @@ export type ProclamationUncheckedUpdateManyWithoutSolfeggioFrequencyInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string content?: Prisma.StringFieldUpdateOperationsInput | string
categoryId?: Prisma.IntFieldUpdateOperationsInput | number categoryId?: Prisma.IntFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number groupId?: Prisma.IntFieldUpdateOperationsInput | number
} }
export type ProclamationCreateManyCategoryInput = { export type ProclamationCreateManyCategoryInput = {
@@ -786,7 +820,7 @@ export type ProclamationCreateManyCategoryInput = {
name: string name: string
content: string content: string
solfeggioFrequencyId: number solfeggioFrequencyId: number
subscriptionLevelId: number groupId: number
} }
export type ProclamationUpdateWithoutCategoryInput = { export type ProclamationUpdateWithoutCategoryInput = {
@@ -794,8 +828,8 @@ export type ProclamationUpdateWithoutCategoryInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
group?: Prisma.ProclamationGroupUpdateOneRequiredWithoutProclamationsNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput
} }
export type ProclamationUncheckedUpdateWithoutCategoryInput = { export type ProclamationUncheckedUpdateWithoutCategoryInput = {
@@ -803,7 +837,7 @@ export type ProclamationUncheckedUpdateWithoutCategoryInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number groupId?: Prisma.IntFieldUpdateOperationsInput | number
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
} }
@@ -812,41 +846,7 @@ export type ProclamationUncheckedUpdateManyWithoutCategoryInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number groupId?: 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
} }
@@ -886,11 +886,11 @@ export type ProclamationSelect<ExtArgs extends runtime.Types.Extensions.Internal
content?: boolean content?: boolean
solfeggioFrequencyId?: boolean solfeggioFrequencyId?: boolean
categoryId?: boolean categoryId?: boolean
subscriptionLevelId?: boolean groupId?: boolean
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs> solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs> category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
group?: boolean | Prisma.ProclamationGroupDefaultArgs<ExtArgs>
userProclamations?: boolean | Prisma.Proclamation$userProclamationsArgs<ExtArgs> userProclamations?: boolean | Prisma.Proclamation$userProclamationsArgs<ExtArgs>
subscriptionLevel?: boolean | Prisma.SubscriptionLevelDefaultArgs<ExtArgs>
_count?: boolean | Prisma.ProclamationCountOutputTypeDefaultArgs<ExtArgs> _count?: boolean | Prisma.ProclamationCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["proclamation"]> }, ExtArgs["result"]["proclamation"]>
@@ -900,10 +900,10 @@ export type ProclamationSelectCreateManyAndReturn<ExtArgs extends runtime.Types.
content?: boolean content?: boolean
solfeggioFrequencyId?: boolean solfeggioFrequencyId?: boolean
categoryId?: boolean categoryId?: boolean
subscriptionLevelId?: boolean groupId?: boolean
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs> solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs> category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
subscriptionLevel?: boolean | Prisma.SubscriptionLevelDefaultArgs<ExtArgs> group?: boolean | Prisma.ProclamationGroupDefaultArgs<ExtArgs>
}, ExtArgs["result"]["proclamation"]> }, ExtArgs["result"]["proclamation"]>
export type ProclamationSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ 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 content?: boolean
solfeggioFrequencyId?: boolean solfeggioFrequencyId?: boolean
categoryId?: boolean categoryId?: boolean
subscriptionLevelId?: boolean groupId?: boolean
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs> solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs> category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
subscriptionLevel?: boolean | Prisma.SubscriptionLevelDefaultArgs<ExtArgs> group?: boolean | Prisma.ProclamationGroupDefaultArgs<ExtArgs>
}, ExtArgs["result"]["proclamation"]> }, ExtArgs["result"]["proclamation"]>
export type ProclamationSelectScalar = { export type ProclamationSelectScalar = {
@@ -924,26 +924,26 @@ export type ProclamationSelectScalar = {
content?: boolean content?: boolean
solfeggioFrequencyId?: boolean solfeggioFrequencyId?: boolean
categoryId?: 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> = { export type ProclamationInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs> solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs> category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
group?: boolean | Prisma.ProclamationGroupDefaultArgs<ExtArgs>
userProclamations?: boolean | Prisma.Proclamation$userProclamationsArgs<ExtArgs> userProclamations?: boolean | Prisma.Proclamation$userProclamationsArgs<ExtArgs>
subscriptionLevel?: boolean | Prisma.SubscriptionLevelDefaultArgs<ExtArgs>
_count?: boolean | Prisma.ProclamationCountOutputTypeDefaultArgs<ExtArgs> _count?: boolean | Prisma.ProclamationCountOutputTypeDefaultArgs<ExtArgs>
} }
export type ProclamationIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type ProclamationIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs> solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<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> = { export type ProclamationIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs> solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<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> = { 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: { objects: {
solfeggioFrequency: Prisma.$SolfeggioFrequencyPayload<ExtArgs> solfeggioFrequency: Prisma.$SolfeggioFrequencyPayload<ExtArgs>
category: Prisma.$ProclamationCategoryPayload<ExtArgs> category: Prisma.$ProclamationCategoryPayload<ExtArgs>
group: Prisma.$ProclamationGroupPayload<ExtArgs>
userProclamations: Prisma.$UserProclamationsPayload<ExtArgs>[] userProclamations: Prisma.$UserProclamationsPayload<ExtArgs>[]
subscriptionLevel: Prisma.$SubscriptionLevelPayload<ExtArgs>
} }
scalars: runtime.Types.Extensions.GetPayloadResult<{ scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string id: string
@@ -960,7 +960,7 @@ export type $ProclamationPayload<ExtArgs extends runtime.Types.Extensions.Intern
content: string content: string
solfeggioFrequencyId: number solfeggioFrequencyId: number
categoryId: number categoryId: number
subscriptionLevelId: number groupId: number
}, ExtArgs["result"]["proclamation"]> }, ExtArgs["result"]["proclamation"]>
composites: {} composites: {}
} }
@@ -1357,8 +1357,8 @@ export interface Prisma__ProclamationClient<T, Null = never, ExtArgs extends run
readonly [Symbol.toStringTag]: "PrismaPromise" 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> 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> 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> 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. * Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved. * @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 content: Prisma.FieldRef<"Proclamation", 'String'>
readonly solfeggioFrequencyId: Prisma.FieldRef<"Proclamation", 'Int'> readonly solfeggioFrequencyId: Prisma.FieldRef<"Proclamation", 'Int'>
readonly categoryId: 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 = { export type ProclamationCategoryAvgAggregateOutputType = {
id: number | null id: number | null
sortOrder: number | null sortOrder: number | null
parentId: number | null
} }
export type ProclamationCategorySumAggregateOutputType = { export type ProclamationCategorySumAggregateOutputType = {
id: number | null id: number | null
sortOrder: number | null sortOrder: number | null
parentId: number | null
} }
export type ProclamationCategoryMinAggregateOutputType = { export type ProclamationCategoryMinAggregateOutputType = {
id: number | null id: number | null
name: string | null name: string | null
sortOrder: number | null sortOrder: number | null
parentId: number | null
} }
export type ProclamationCategoryMaxAggregateOutputType = { export type ProclamationCategoryMaxAggregateOutputType = {
id: number | null id: number | null
name: string | null name: string | null
sortOrder: number | null sortOrder: number | null
parentId: number | null
} }
export type ProclamationCategoryCountAggregateOutputType = { export type ProclamationCategoryCountAggregateOutputType = {
id: number id: number
name: number name: number
sortOrder: number sortOrder: number
parentId: number
_all: number _all: number
} }
@@ -64,34 +59,29 @@ export type ProclamationCategoryCountAggregateOutputType = {
export type ProclamationCategoryAvgAggregateInputType = { export type ProclamationCategoryAvgAggregateInputType = {
id?: true id?: true
sortOrder?: true sortOrder?: true
parentId?: true
} }
export type ProclamationCategorySumAggregateInputType = { export type ProclamationCategorySumAggregateInputType = {
id?: true id?: true
sortOrder?: true sortOrder?: true
parentId?: true
} }
export type ProclamationCategoryMinAggregateInputType = { export type ProclamationCategoryMinAggregateInputType = {
id?: true id?: true
name?: true name?: true
sortOrder?: true sortOrder?: true
parentId?: true
} }
export type ProclamationCategoryMaxAggregateInputType = { export type ProclamationCategoryMaxAggregateInputType = {
id?: true id?: true
name?: true name?: true
sortOrder?: true sortOrder?: true
parentId?: true
} }
export type ProclamationCategoryCountAggregateInputType = { export type ProclamationCategoryCountAggregateInputType = {
id?: true id?: true
name?: true name?: true
sortOrder?: true sortOrder?: true
parentId?: true
_all?: true _all?: true
} }
@@ -185,7 +175,6 @@ export type ProclamationCategoryGroupByOutputType = {
id: number id: number
name: string name: string
sortOrder: number sortOrder: number
parentId: number | null
_count: ProclamationCategoryCountAggregateOutputType | null _count: ProclamationCategoryCountAggregateOutputType | null
_avg: ProclamationCategoryAvgAggregateOutputType | null _avg: ProclamationCategoryAvgAggregateOutputType | null
_sum: ProclamationCategorySumAggregateOutputType | null _sum: ProclamationCategorySumAggregateOutputType | null
@@ -215,44 +204,30 @@ export type ProclamationCategoryWhereInput = {
id?: Prisma.IntFilter<"ProclamationCategory"> | number id?: Prisma.IntFilter<"ProclamationCategory"> | number
name?: Prisma.StringFilter<"ProclamationCategory"> | string name?: Prisma.StringFilter<"ProclamationCategory"> | string
sortOrder?: Prisma.IntFilter<"ProclamationCategory"> | number 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 proclamations?: Prisma.ProclamationListRelationFilter
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelListRelationFilter
} }
export type ProclamationCategoryOrderByWithRelationInput = { export type ProclamationCategoryOrderByWithRelationInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
name?: Prisma.SortOrder name?: Prisma.SortOrder
sortOrder?: Prisma.SortOrder sortOrder?: Prisma.SortOrder
parentId?: Prisma.SortOrderInput | Prisma.SortOrder
parent?: Prisma.ProclamationCategoryOrderByWithRelationInput
children?: Prisma.ProclamationCategoryOrderByRelationAggregateInput
proclamations?: Prisma.ProclamationOrderByRelationAggregateInput proclamations?: Prisma.ProclamationOrderByRelationAggregateInput
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelOrderByRelationAggregateInput
} }
export type ProclamationCategoryWhereUniqueInput = Prisma.AtLeast<{ export type ProclamationCategoryWhereUniqueInput = Prisma.AtLeast<{
id?: number id?: number
parentId_name?: Prisma.ProclamationCategoryParentIdNameCompoundUniqueInput name?: string
AND?: Prisma.ProclamationCategoryWhereInput | Prisma.ProclamationCategoryWhereInput[] AND?: Prisma.ProclamationCategoryWhereInput | Prisma.ProclamationCategoryWhereInput[]
OR?: Prisma.ProclamationCategoryWhereInput[] OR?: Prisma.ProclamationCategoryWhereInput[]
NOT?: Prisma.ProclamationCategoryWhereInput | Prisma.ProclamationCategoryWhereInput[] NOT?: Prisma.ProclamationCategoryWhereInput | Prisma.ProclamationCategoryWhereInput[]
name?: Prisma.StringFilter<"ProclamationCategory"> | string
sortOrder?: Prisma.IntFilter<"ProclamationCategory"> | number 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 proclamations?: Prisma.ProclamationListRelationFilter
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelListRelationFilter }, "id" | "name">
}, "id" | "parentId_name">
export type ProclamationCategoryOrderByWithAggregationInput = { export type ProclamationCategoryOrderByWithAggregationInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
name?: Prisma.SortOrder name?: Prisma.SortOrder
sortOrder?: Prisma.SortOrder sortOrder?: Prisma.SortOrder
parentId?: Prisma.SortOrderInput | Prisma.SortOrder
_count?: Prisma.ProclamationCategoryCountOrderByAggregateInput _count?: Prisma.ProclamationCategoryCountOrderByAggregateInput
_avg?: Prisma.ProclamationCategoryAvgOrderByAggregateInput _avg?: Prisma.ProclamationCategoryAvgOrderByAggregateInput
_max?: Prisma.ProclamationCategoryMaxOrderByAggregateInput _max?: Prisma.ProclamationCategoryMaxOrderByAggregateInput
@@ -267,52 +242,38 @@ export type ProclamationCategoryScalarWhereWithAggregatesInput = {
id?: Prisma.IntWithAggregatesFilter<"ProclamationCategory"> | number id?: Prisma.IntWithAggregatesFilter<"ProclamationCategory"> | number
name?: Prisma.StringWithAggregatesFilter<"ProclamationCategory"> | string name?: Prisma.StringWithAggregatesFilter<"ProclamationCategory"> | string
sortOrder?: Prisma.IntWithAggregatesFilter<"ProclamationCategory"> | number sortOrder?: Prisma.IntWithAggregatesFilter<"ProclamationCategory"> | number
parentId?: Prisma.IntNullableWithAggregatesFilter<"ProclamationCategory"> | number | null
} }
export type ProclamationCategoryCreateInput = { export type ProclamationCategoryCreateInput = {
name: string name: string
sortOrder?: number sortOrder?: number
parent?: Prisma.ProclamationCategoryCreateNestedOneWithoutChildrenInput
children?: Prisma.ProclamationCategoryCreateNestedManyWithoutParentInput
proclamations?: Prisma.ProclamationCreateNestedManyWithoutCategoryInput proclamations?: Prisma.ProclamationCreateNestedManyWithoutCategoryInput
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelCreateNestedManyWithoutCategoryInput
} }
export type ProclamationCategoryUncheckedCreateInput = { export type ProclamationCategoryUncheckedCreateInput = {
id?: number id?: number
name: string name: string
sortOrder?: number sortOrder?: number
parentId?: number | null
children?: Prisma.ProclamationCategoryUncheckedCreateNestedManyWithoutParentInput
proclamations?: Prisma.ProclamationUncheckedCreateNestedManyWithoutCategoryInput proclamations?: Prisma.ProclamationUncheckedCreateNestedManyWithoutCategoryInput
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUncheckedCreateNestedManyWithoutCategoryInput
} }
export type ProclamationCategoryUpdateInput = { export type ProclamationCategoryUpdateInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
parent?: Prisma.ProclamationCategoryUpdateOneWithoutChildrenNestedInput
children?: Prisma.ProclamationCategoryUpdateManyWithoutParentNestedInput
proclamations?: Prisma.ProclamationUpdateManyWithoutCategoryNestedInput proclamations?: Prisma.ProclamationUpdateManyWithoutCategoryNestedInput
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUpdateManyWithoutCategoryNestedInput
} }
export type ProclamationCategoryUncheckedUpdateInput = { export type ProclamationCategoryUncheckedUpdateInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number id?: Prisma.IntFieldUpdateOperationsInput | number
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
parentId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
children?: Prisma.ProclamationCategoryUncheckedUpdateManyWithoutParentNestedInput
proclamations?: Prisma.ProclamationUncheckedUpdateManyWithoutCategoryNestedInput proclamations?: Prisma.ProclamationUncheckedUpdateManyWithoutCategoryNestedInput
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUncheckedUpdateManyWithoutCategoryNestedInput
} }
export type ProclamationCategoryCreateManyInput = { export type ProclamationCategoryCreateManyInput = {
id?: number id?: number
name: string name: string
sortOrder?: number sortOrder?: number
parentId?: number | null
} }
export type ProclamationCategoryUpdateManyMutationInput = { export type ProclamationCategoryUpdateManyMutationInput = {
@@ -324,7 +285,6 @@ export type ProclamationCategoryUncheckedUpdateManyInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number id?: Prisma.IntFieldUpdateOperationsInput | number
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
parentId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
} }
export type ProclamationCategoryScalarRelationFilter = { export type ProclamationCategoryScalarRelationFilter = {
@@ -332,57 +292,32 @@ export type ProclamationCategoryScalarRelationFilter = {
isNot?: Prisma.ProclamationCategoryWhereInput 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 = { export type ProclamationCategoryCountOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
name?: Prisma.SortOrder name?: Prisma.SortOrder
sortOrder?: Prisma.SortOrder sortOrder?: Prisma.SortOrder
parentId?: Prisma.SortOrder
} }
export type ProclamationCategoryAvgOrderByAggregateInput = { export type ProclamationCategoryAvgOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
sortOrder?: Prisma.SortOrder sortOrder?: Prisma.SortOrder
parentId?: Prisma.SortOrder
} }
export type ProclamationCategoryMaxOrderByAggregateInput = { export type ProclamationCategoryMaxOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
name?: Prisma.SortOrder name?: Prisma.SortOrder
sortOrder?: Prisma.SortOrder sortOrder?: Prisma.SortOrder
parentId?: Prisma.SortOrder
} }
export type ProclamationCategoryMinOrderByAggregateInput = { export type ProclamationCategoryMinOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
name?: Prisma.SortOrder name?: Prisma.SortOrder
sortOrder?: Prisma.SortOrder sortOrder?: Prisma.SortOrder
parentId?: Prisma.SortOrder
} }
export type ProclamationCategorySumOrderByAggregateInput = { export type ProclamationCategorySumOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
sortOrder?: Prisma.SortOrder sortOrder?: Prisma.SortOrder
parentId?: Prisma.SortOrder
} }
export type ProclamationCategoryCreateNestedOneWithoutProclamationsInput = { export type ProclamationCategoryCreateNestedOneWithoutProclamationsInput = {
@@ -399,93 +334,15 @@ export type ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
update?: Prisma.XOR<Prisma.XOR<Prisma.ProclamationCategoryUpdateToOneWithWhereWithoutProclamationsInput, Prisma.ProclamationCategoryUpdateWithoutProclamationsInput>, Prisma.ProclamationCategoryUncheckedUpdateWithoutProclamationsInput> 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 = { export type ProclamationCategoryCreateWithoutProclamationsInput = {
name: string name: string
sortOrder?: number sortOrder?: number
parent?: Prisma.ProclamationCategoryCreateNestedOneWithoutChildrenInput
children?: Prisma.ProclamationCategoryCreateNestedManyWithoutParentInput
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelCreateNestedManyWithoutCategoryInput
} }
export type ProclamationCategoryUncheckedCreateWithoutProclamationsInput = { export type ProclamationCategoryUncheckedCreateWithoutProclamationsInput = {
id?: number id?: number
name: string name: string
sortOrder?: number sortOrder?: number
parentId?: number | null
children?: Prisma.ProclamationCategoryUncheckedCreateNestedManyWithoutParentInput
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUncheckedCreateNestedManyWithoutCategoryInput
} }
export type ProclamationCategoryCreateOrConnectWithoutProclamationsInput = { export type ProclamationCategoryCreateOrConnectWithoutProclamationsInput = {
@@ -507,200 +364,12 @@ export type ProclamationCategoryUpdateToOneWithWhereWithoutProclamationsInput =
export type ProclamationCategoryUpdateWithoutProclamationsInput = { export type ProclamationCategoryUpdateWithoutProclamationsInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
parent?: Prisma.ProclamationCategoryUpdateOneWithoutChildrenNestedInput
children?: Prisma.ProclamationCategoryUpdateManyWithoutParentNestedInput
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUpdateManyWithoutCategoryNestedInput
} }
export type ProclamationCategoryUncheckedUpdateWithoutProclamationsInput = { export type ProclamationCategoryUncheckedUpdateWithoutProclamationsInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number id?: Prisma.IntFieldUpdateOperationsInput | number
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number 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 = { export type ProclamationCategoryCountOutputType = {
children: number
proclamations: number proclamations: number
subscriptionLevels: number
} }
export type ProclamationCategoryCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type ProclamationCategoryCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
children?: boolean | ProclamationCategoryCountOutputTypeCountChildrenArgs
proclamations?: boolean | ProclamationCategoryCountOutputTypeCountProclamationsArgs proclamations?: boolean | ProclamationCategoryCountOutputTypeCountProclamationsArgs
subscriptionLevels?: boolean | ProclamationCategoryCountOutputTypeCountSubscriptionLevelsArgs
} }
/** /**
@@ -730,13 +395,6 @@ export type ProclamationCategoryCountOutputTypeDefaultArgs<ExtArgs extends runti
select?: Prisma.ProclamationCategoryCountOutputTypeSelect<ExtArgs> | null 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 * ProclamationCategoryCountOutputType without action
*/ */
@@ -744,23 +402,12 @@ export type ProclamationCategoryCountOutputTypeCountProclamationsArgs<ExtArgs ex
where?: Prisma.ProclamationWhereInput 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<{ export type ProclamationCategorySelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean id?: boolean
name?: boolean name?: boolean
sortOrder?: boolean sortOrder?: boolean
parentId?: boolean
parent?: boolean | Prisma.ProclamationCategory$parentArgs<ExtArgs>
children?: boolean | Prisma.ProclamationCategory$childrenArgs<ExtArgs>
proclamations?: boolean | Prisma.ProclamationCategory$proclamationsArgs<ExtArgs> proclamations?: boolean | Prisma.ProclamationCategory$proclamationsArgs<ExtArgs>
subscriptionLevels?: boolean | Prisma.ProclamationCategory$subscriptionLevelsArgs<ExtArgs>
_count?: boolean | Prisma.ProclamationCategoryCountOutputTypeDefaultArgs<ExtArgs> _count?: boolean | Prisma.ProclamationCategoryCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["proclamationCategory"]> }, ExtArgs["result"]["proclamationCategory"]>
@@ -768,53 +415,37 @@ export type ProclamationCategorySelectCreateManyAndReturn<ExtArgs extends runtim
id?: boolean id?: boolean
name?: boolean name?: boolean
sortOrder?: boolean sortOrder?: boolean
parentId?: boolean
parent?: boolean | Prisma.ProclamationCategory$parentArgs<ExtArgs>
}, ExtArgs["result"]["proclamationCategory"]> }, ExtArgs["result"]["proclamationCategory"]>
export type ProclamationCategorySelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ export type ProclamationCategorySelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean id?: boolean
name?: boolean name?: boolean
sortOrder?: boolean sortOrder?: boolean
parentId?: boolean
parent?: boolean | Prisma.ProclamationCategory$parentArgs<ExtArgs>
}, ExtArgs["result"]["proclamationCategory"]> }, ExtArgs["result"]["proclamationCategory"]>
export type ProclamationCategorySelectScalar = { export type ProclamationCategorySelectScalar = {
id?: boolean id?: boolean
name?: boolean name?: boolean
sortOrder?: 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> = { 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> proclamations?: boolean | Prisma.ProclamationCategory$proclamationsArgs<ExtArgs>
subscriptionLevels?: boolean | Prisma.ProclamationCategory$subscriptionLevelsArgs<ExtArgs>
_count?: boolean | Prisma.ProclamationCategoryCountOutputTypeDefaultArgs<ExtArgs> _count?: boolean | Prisma.ProclamationCategoryCountOutputTypeDefaultArgs<ExtArgs>
} }
export type ProclamationCategoryIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { 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> = {}
}
export type ProclamationCategoryIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
parent?: boolean | Prisma.ProclamationCategory$parentArgs<ExtArgs>
}
export type $ProclamationCategoryPayload<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" name: "ProclamationCategory"
objects: { objects: {
parent: Prisma.$ProclamationCategoryPayload<ExtArgs> | null
children: Prisma.$ProclamationCategoryPayload<ExtArgs>[]
proclamations: Prisma.$ProclamationPayload<ExtArgs>[] proclamations: Prisma.$ProclamationPayload<ExtArgs>[]
subscriptionLevels: Prisma.$ProclamationCategorySubscriptionLevelPayload<ExtArgs>[]
} }
scalars: runtime.Types.Extensions.GetPayloadResult<{ scalars: runtime.Types.Extensions.GetPayloadResult<{
id: number id: number
name: string name: string
sortOrder: number sortOrder: number
parentId: number | null
}, ExtArgs["result"]["proclamationCategory"]> }, ExtArgs["result"]["proclamationCategory"]>
composites: {} 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> { 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" 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> 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. * Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved. * @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 id: Prisma.FieldRef<"ProclamationCategory", 'Int'>
readonly name: Prisma.FieldRef<"ProclamationCategory", 'String'> readonly name: Prisma.FieldRef<"ProclamationCategory", 'String'>
readonly sortOrder: Prisma.FieldRef<"ProclamationCategory", 'Int'> 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[] data: Prisma.ProclamationCategoryCreateManyInput | Prisma.ProclamationCategoryCreateManyInput[]
skipDuplicates?: boolean 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 how many ProclamationCategories to update.
*/ */
limit?: number 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 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 * ProclamationCategory.proclamations
*/ */
@@ -1713,30 +1289,6 @@ export type ProclamationCategory$proclamationsArgs<ExtArgs extends runtime.Types
distinct?: Prisma.ProclamationScalarFieldEnum | Prisma.ProclamationScalarFieldEnum[] 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 * ProclamationCategory without action
*/ */
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+187 -76
View File
@@ -27,6 +27,7 @@ export type AggregateProfile = {
export type ProfileMinAggregateOutputType = { export type ProfileMinAggregateOutputType = {
id: string | null id: string | null
userId: string | null userId: string | null
email: string | null
firstName: string | null firstName: string | null
lastName: string | null lastName: string | null
phoneNumber: string | null phoneNumber: string | null
@@ -36,6 +37,7 @@ export type ProfileMinAggregateOutputType = {
export type ProfileMaxAggregateOutputType = { export type ProfileMaxAggregateOutputType = {
id: string | null id: string | null
userId: string | null userId: string | null
email: string | null
firstName: string | null firstName: string | null
lastName: string | null lastName: string | null
phoneNumber: string | null phoneNumber: string | null
@@ -45,6 +47,7 @@ export type ProfileMaxAggregateOutputType = {
export type ProfileCountAggregateOutputType = { export type ProfileCountAggregateOutputType = {
id: number id: number
userId: number userId: number
email: number
firstName: number firstName: number
lastName: number lastName: number
phoneNumber: number phoneNumber: number
@@ -56,6 +59,7 @@ export type ProfileCountAggregateOutputType = {
export type ProfileMinAggregateInputType = { export type ProfileMinAggregateInputType = {
id?: true id?: true
userId?: true userId?: true
email?: true
firstName?: true firstName?: true
lastName?: true lastName?: true
phoneNumber?: true phoneNumber?: true
@@ -65,6 +69,7 @@ export type ProfileMinAggregateInputType = {
export type ProfileMaxAggregateInputType = { export type ProfileMaxAggregateInputType = {
id?: true id?: true
userId?: true userId?: true
email?: true
firstName?: true firstName?: true
lastName?: true lastName?: true
phoneNumber?: true phoneNumber?: true
@@ -74,6 +79,7 @@ export type ProfileMaxAggregateInputType = {
export type ProfileCountAggregateInputType = { export type ProfileCountAggregateInputType = {
id?: true id?: true
userId?: true userId?: true
email?: true
firstName?: true firstName?: true
lastName?: true lastName?: true
phoneNumber?: true phoneNumber?: true
@@ -155,7 +161,8 @@ export type ProfileGroupByArgs<ExtArgs extends runtime.Types.Extensions.Internal
export type ProfileGroupByOutputType = { export type ProfileGroupByOutputType = {
id: string id: string
userId: string userId: string | null
email: string | null
firstName: string firstName: string
lastName: string lastName: string
phoneNumber: string phoneNumber: string
@@ -185,18 +192,20 @@ export type ProfileWhereInput = {
OR?: Prisma.ProfileWhereInput[] OR?: Prisma.ProfileWhereInput[]
NOT?: Prisma.ProfileWhereInput | Prisma.ProfileWhereInput[] NOT?: Prisma.ProfileWhereInput | Prisma.ProfileWhereInput[]
id?: Prisma.StringFilter<"Profile"> | string 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 firstName?: Prisma.StringFilter<"Profile"> | string
lastName?: Prisma.StringFilter<"Profile"> | string lastName?: Prisma.StringFilter<"Profile"> | string
phoneNumber?: Prisma.StringFilter<"Profile"> | string phoneNumber?: Prisma.StringFilter<"Profile"> | string
ghlContactId?: Prisma.StringNullableFilter<"Profile"> | string | null ghlContactId?: Prisma.StringNullableFilter<"Profile"> | string | null
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput> user?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
addresses?: Prisma.AddressListRelationFilter addresses?: Prisma.AddressListRelationFilter
} }
export type ProfileOrderByWithRelationInput = { export type ProfileOrderByWithRelationInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
userId?: Prisma.SortOrder userId?: Prisma.SortOrderInput | Prisma.SortOrder
email?: Prisma.SortOrderInput | Prisma.SortOrder
firstName?: Prisma.SortOrder firstName?: Prisma.SortOrder
lastName?: Prisma.SortOrder lastName?: Prisma.SortOrder
phoneNumber?: Prisma.SortOrder phoneNumber?: Prisma.SortOrder
@@ -207,21 +216,23 @@ export type ProfileOrderByWithRelationInput = {
export type ProfileWhereUniqueInput = Prisma.AtLeast<{ export type ProfileWhereUniqueInput = Prisma.AtLeast<{
id?: string id?: string
userId?: string
AND?: Prisma.ProfileWhereInput | Prisma.ProfileWhereInput[] AND?: Prisma.ProfileWhereInput | Prisma.ProfileWhereInput[]
OR?: Prisma.ProfileWhereInput[] OR?: Prisma.ProfileWhereInput[]
NOT?: Prisma.ProfileWhereInput | Prisma.ProfileWhereInput[] NOT?: Prisma.ProfileWhereInput | Prisma.ProfileWhereInput[]
userId?: Prisma.StringNullableFilter<"Profile"> | string | null
email?: Prisma.StringNullableFilter<"Profile"> | string | null
firstName?: Prisma.StringFilter<"Profile"> | string firstName?: Prisma.StringFilter<"Profile"> | string
lastName?: Prisma.StringFilter<"Profile"> | string lastName?: Prisma.StringFilter<"Profile"> | string
phoneNumber?: Prisma.StringFilter<"Profile"> | string phoneNumber?: Prisma.StringFilter<"Profile"> | string
ghlContactId?: Prisma.StringNullableFilter<"Profile"> | string | null ghlContactId?: Prisma.StringNullableFilter<"Profile"> | string | null
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput> user?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
addresses?: Prisma.AddressListRelationFilter addresses?: Prisma.AddressListRelationFilter
}, "id" | "userId"> }, "id">
export type ProfileOrderByWithAggregationInput = { export type ProfileOrderByWithAggregationInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
userId?: Prisma.SortOrder userId?: Prisma.SortOrderInput | Prisma.SortOrder
email?: Prisma.SortOrderInput | Prisma.SortOrder
firstName?: Prisma.SortOrder firstName?: Prisma.SortOrder
lastName?: Prisma.SortOrder lastName?: Prisma.SortOrder
phoneNumber?: Prisma.SortOrder phoneNumber?: Prisma.SortOrder
@@ -236,7 +247,8 @@ export type ProfileScalarWhereWithAggregatesInput = {
OR?: Prisma.ProfileScalarWhereWithAggregatesInput[] OR?: Prisma.ProfileScalarWhereWithAggregatesInput[]
NOT?: Prisma.ProfileScalarWhereWithAggregatesInput | Prisma.ProfileScalarWhereWithAggregatesInput[] NOT?: Prisma.ProfileScalarWhereWithAggregatesInput | Prisma.ProfileScalarWhereWithAggregatesInput[]
id?: Prisma.StringWithAggregatesFilter<"Profile"> | string 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 firstName?: Prisma.StringWithAggregatesFilter<"Profile"> | string
lastName?: Prisma.StringWithAggregatesFilter<"Profile"> | string lastName?: Prisma.StringWithAggregatesFilter<"Profile"> | string
phoneNumber?: Prisma.StringWithAggregatesFilter<"Profile"> | string phoneNumber?: Prisma.StringWithAggregatesFilter<"Profile"> | string
@@ -245,17 +257,19 @@ export type ProfileScalarWhereWithAggregatesInput = {
export type ProfileCreateInput = { export type ProfileCreateInput = {
id?: string id?: string
email?: string | null
firstName: string firstName: string
lastName: string lastName: string
phoneNumber: string phoneNumber: string
ghlContactId?: string | null ghlContactId?: string | null
user: Prisma.UserCreateNestedOneWithoutProfileInput user?: Prisma.UserCreateNestedOneWithoutProfilesInput
addresses?: Prisma.AddressCreateNestedManyWithoutProfileInput addresses?: Prisma.AddressCreateNestedManyWithoutProfileInput
} }
export type ProfileUncheckedCreateInput = { export type ProfileUncheckedCreateInput = {
id?: string id?: string
userId: string userId?: string | null
email?: string | null
firstName: string firstName: string
lastName: string lastName: string
phoneNumber: string phoneNumber: string
@@ -265,17 +279,19 @@ export type ProfileUncheckedCreateInput = {
export type ProfileUpdateInput = { export type ProfileUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
firstName?: Prisma.StringFieldUpdateOperationsInput | string firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
user?: Prisma.UserUpdateOneRequiredWithoutProfileNestedInput user?: Prisma.UserUpdateOneWithoutProfilesNestedInput
addresses?: Prisma.AddressUpdateManyWithoutProfileNestedInput addresses?: Prisma.AddressUpdateManyWithoutProfileNestedInput
} }
export type ProfileUncheckedUpdateInput = { export type ProfileUncheckedUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
userId?: Prisma.StringFieldUpdateOperationsInput | string userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
firstName?: Prisma.StringFieldUpdateOperationsInput | string firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
@@ -285,7 +301,8 @@ export type ProfileUncheckedUpdateInput = {
export type ProfileCreateManyInput = { export type ProfileCreateManyInput = {
id?: string id?: string
userId: string userId?: string | null
email?: string | null
firstName: string firstName: string
lastName: string lastName: string
phoneNumber: string phoneNumber: string
@@ -294,6 +311,7 @@ export type ProfileCreateManyInput = {
export type ProfileUpdateManyMutationInput = { export type ProfileUpdateManyMutationInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
firstName?: Prisma.StringFieldUpdateOperationsInput | string firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
@@ -302,21 +320,28 @@ export type ProfileUpdateManyMutationInput = {
export type ProfileUncheckedUpdateManyInput = { export type ProfileUncheckedUpdateManyInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
userId?: Prisma.StringFieldUpdateOperationsInput | string userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
firstName?: Prisma.StringFieldUpdateOperationsInput | string firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
} }
export type ProfileNullableScalarRelationFilter = { export type ProfileListRelationFilter = {
is?: Prisma.ProfileWhereInput | null every?: Prisma.ProfileWhereInput
isNot?: Prisma.ProfileWhereInput | null some?: Prisma.ProfileWhereInput
none?: Prisma.ProfileWhereInput
}
export type ProfileOrderByRelationAggregateInput = {
_count?: Prisma.SortOrder
} }
export type ProfileCountOrderByAggregateInput = { export type ProfileCountOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
userId?: Prisma.SortOrder userId?: Prisma.SortOrder
email?: Prisma.SortOrder
firstName?: Prisma.SortOrder firstName?: Prisma.SortOrder
lastName?: Prisma.SortOrder lastName?: Prisma.SortOrder
phoneNumber?: Prisma.SortOrder phoneNumber?: Prisma.SortOrder
@@ -326,6 +351,7 @@ export type ProfileCountOrderByAggregateInput = {
export type ProfileMaxOrderByAggregateInput = { export type ProfileMaxOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
userId?: Prisma.SortOrder userId?: Prisma.SortOrder
email?: Prisma.SortOrder
firstName?: Prisma.SortOrder firstName?: Prisma.SortOrder
lastName?: Prisma.SortOrder lastName?: Prisma.SortOrder
phoneNumber?: Prisma.SortOrder phoneNumber?: Prisma.SortOrder
@@ -335,6 +361,7 @@ export type ProfileMaxOrderByAggregateInput = {
export type ProfileMinOrderByAggregateInput = { export type ProfileMinOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
userId?: Prisma.SortOrder userId?: Prisma.SortOrder
email?: Prisma.SortOrder
firstName?: Prisma.SortOrder firstName?: Prisma.SortOrder
lastName?: Prisma.SortOrder lastName?: Prisma.SortOrder
phoneNumber?: Prisma.SortOrder phoneNumber?: Prisma.SortOrder
@@ -346,36 +373,46 @@ export type ProfileScalarRelationFilter = {
isNot?: Prisma.ProfileWhereInput isNot?: Prisma.ProfileWhereInput
} }
export type ProfileCreateNestedOneWithoutUserInput = { export type ProfileCreateNestedManyWithoutUserInput = {
create?: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput> create?: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput> | Prisma.ProfileCreateWithoutUserInput[] | Prisma.ProfileUncheckedCreateWithoutUserInput[]
connectOrCreate?: Prisma.ProfileCreateOrConnectWithoutUserInput connectOrCreate?: Prisma.ProfileCreateOrConnectWithoutUserInput | Prisma.ProfileCreateOrConnectWithoutUserInput[]
connect?: Prisma.ProfileWhereUniqueInput createMany?: Prisma.ProfileCreateManyUserInputEnvelope
connect?: Prisma.ProfileWhereUniqueInput | Prisma.ProfileWhereUniqueInput[]
} }
export type ProfileUncheckedCreateNestedOneWithoutUserInput = { export type ProfileUncheckedCreateNestedManyWithoutUserInput = {
create?: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput> create?: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput> | Prisma.ProfileCreateWithoutUserInput[] | Prisma.ProfileUncheckedCreateWithoutUserInput[]
connectOrCreate?: Prisma.ProfileCreateOrConnectWithoutUserInput connectOrCreate?: Prisma.ProfileCreateOrConnectWithoutUserInput | Prisma.ProfileCreateOrConnectWithoutUserInput[]
connect?: Prisma.ProfileWhereUniqueInput createMany?: Prisma.ProfileCreateManyUserInputEnvelope
connect?: Prisma.ProfileWhereUniqueInput | Prisma.ProfileWhereUniqueInput[]
} }
export type ProfileUpdateOneWithoutUserNestedInput = { export type ProfileUpdateManyWithoutUserNestedInput = {
create?: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput> create?: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput> | Prisma.ProfileCreateWithoutUserInput[] | Prisma.ProfileUncheckedCreateWithoutUserInput[]
connectOrCreate?: Prisma.ProfileCreateOrConnectWithoutUserInput connectOrCreate?: Prisma.ProfileCreateOrConnectWithoutUserInput | Prisma.ProfileCreateOrConnectWithoutUserInput[]
upsert?: Prisma.ProfileUpsertWithoutUserInput upsert?: Prisma.ProfileUpsertWithWhereUniqueWithoutUserInput | Prisma.ProfileUpsertWithWhereUniqueWithoutUserInput[]
disconnect?: Prisma.ProfileWhereInput | boolean createMany?: Prisma.ProfileCreateManyUserInputEnvelope
delete?: Prisma.ProfileWhereInput | boolean set?: Prisma.ProfileWhereUniqueInput | Prisma.ProfileWhereUniqueInput[]
connect?: Prisma.ProfileWhereUniqueInput disconnect?: Prisma.ProfileWhereUniqueInput | Prisma.ProfileWhereUniqueInput[]
update?: Prisma.XOR<Prisma.XOR<Prisma.ProfileUpdateToOneWithWhereWithoutUserInput, Prisma.ProfileUpdateWithoutUserInput>, Prisma.ProfileUncheckedUpdateWithoutUserInput> 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 = { export type ProfileUncheckedUpdateManyWithoutUserNestedInput = {
create?: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput> create?: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput> | Prisma.ProfileCreateWithoutUserInput[] | Prisma.ProfileUncheckedCreateWithoutUserInput[]
connectOrCreate?: Prisma.ProfileCreateOrConnectWithoutUserInput connectOrCreate?: Prisma.ProfileCreateOrConnectWithoutUserInput | Prisma.ProfileCreateOrConnectWithoutUserInput[]
upsert?: Prisma.ProfileUpsertWithoutUserInput upsert?: Prisma.ProfileUpsertWithWhereUniqueWithoutUserInput | Prisma.ProfileUpsertWithWhereUniqueWithoutUserInput[]
disconnect?: Prisma.ProfileWhereInput | boolean createMany?: Prisma.ProfileCreateManyUserInputEnvelope
delete?: Prisma.ProfileWhereInput | boolean set?: Prisma.ProfileWhereUniqueInput | Prisma.ProfileWhereUniqueInput[]
connect?: Prisma.ProfileWhereUniqueInput disconnect?: Prisma.ProfileWhereUniqueInput | Prisma.ProfileWhereUniqueInput[]
update?: Prisma.XOR<Prisma.XOR<Prisma.ProfileUpdateToOneWithWhereWithoutUserInput, Prisma.ProfileUpdateWithoutUserInput>, Prisma.ProfileUncheckedUpdateWithoutUserInput> 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 = { export type ProfileCreateNestedOneWithoutAddressesInput = {
@@ -394,6 +431,7 @@ export type ProfileUpdateOneRequiredWithoutAddressesNestedInput = {
export type ProfileCreateWithoutUserInput = { export type ProfileCreateWithoutUserInput = {
id?: string id?: string
email?: string | null
firstName: string firstName: string
lastName: string lastName: string
phoneNumber: string phoneNumber: string
@@ -403,6 +441,7 @@ export type ProfileCreateWithoutUserInput = {
export type ProfileUncheckedCreateWithoutUserInput = { export type ProfileUncheckedCreateWithoutUserInput = {
id?: string id?: string
email?: string | null
firstName: string firstName: string
lastName: string lastName: string
phoneNumber: string phoneNumber: string
@@ -415,47 +454,54 @@ export type ProfileCreateOrConnectWithoutUserInput = {
create: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput> create: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput>
} }
export type ProfileUpsertWithoutUserInput = { export type ProfileCreateManyUserInputEnvelope = {
update: Prisma.XOR<Prisma.ProfileUpdateWithoutUserInput, Prisma.ProfileUncheckedUpdateWithoutUserInput> data: Prisma.ProfileCreateManyUserInput | Prisma.ProfileCreateManyUserInput[]
create: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput> skipDuplicates?: boolean
where?: Prisma.ProfileWhereInput
} }
export type ProfileUpdateToOneWithWhereWithoutUserInput = { export type ProfileUpsertWithWhereUniqueWithoutUserInput = {
where?: Prisma.ProfileWhereInput 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> data: Prisma.XOR<Prisma.ProfileUpdateWithoutUserInput, Prisma.ProfileUncheckedUpdateWithoutUserInput>
} }
export type ProfileUpdateWithoutUserInput = { export type ProfileUpdateManyWithWhereWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string where: Prisma.ProfileScalarWhereInput
firstName?: Prisma.StringFieldUpdateOperationsInput | string data: Prisma.XOR<Prisma.ProfileUpdateManyMutationInput, Prisma.ProfileUncheckedUpdateManyWithoutUserInput>
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
addresses?: Prisma.AddressUpdateManyWithoutProfileNestedInput
} }
export type ProfileUncheckedUpdateWithoutUserInput = { export type ProfileScalarWhereInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string AND?: Prisma.ProfileScalarWhereInput | Prisma.ProfileScalarWhereInput[]
firstName?: Prisma.StringFieldUpdateOperationsInput | string OR?: Prisma.ProfileScalarWhereInput[]
lastName?: Prisma.StringFieldUpdateOperationsInput | string NOT?: Prisma.ProfileScalarWhereInput | Prisma.ProfileScalarWhereInput[]
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFilter<"Profile"> | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null userId?: Prisma.StringNullableFilter<"Profile"> | string | null
addresses?: Prisma.AddressUncheckedUpdateManyWithoutProfileNestedInput 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 = { export type ProfileCreateWithoutAddressesInput = {
id?: string id?: string
email?: string | null
firstName: string firstName: string
lastName: string lastName: string
phoneNumber: string phoneNumber: string
ghlContactId?: string | null ghlContactId?: string | null
user: Prisma.UserCreateNestedOneWithoutProfileInput user?: Prisma.UserCreateNestedOneWithoutProfilesInput
} }
export type ProfileUncheckedCreateWithoutAddressesInput = { export type ProfileUncheckedCreateWithoutAddressesInput = {
id?: string id?: string
userId: string userId?: string | null
email?: string | null
firstName: string firstName: string
lastName: string lastName: string
phoneNumber: string phoneNumber: string
@@ -480,16 +526,56 @@ export type ProfileUpdateToOneWithWhereWithoutAddressesInput = {
export type ProfileUpdateWithoutAddressesInput = { export type ProfileUpdateWithoutAddressesInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
firstName?: Prisma.StringFieldUpdateOperationsInput | string firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
user?: Prisma.UserUpdateOneRequiredWithoutProfileNestedInput user?: Prisma.UserUpdateOneWithoutProfilesNestedInput
} }
export type ProfileUncheckedUpdateWithoutAddressesInput = { export type ProfileUncheckedUpdateWithoutAddressesInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string 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 firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: 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<{ export type ProfileSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean id?: boolean
userId?: boolean userId?: boolean
email?: boolean
firstName?: boolean firstName?: boolean
lastName?: boolean lastName?: boolean
phoneNumber?: boolean phoneNumber?: boolean
ghlContactId?: boolean ghlContactId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs> user?: boolean | Prisma.Profile$userArgs<ExtArgs>
addresses?: boolean | Prisma.Profile$addressesArgs<ExtArgs> addresses?: boolean | Prisma.Profile$addressesArgs<ExtArgs>
_count?: boolean | Prisma.ProfileCountOutputTypeDefaultArgs<ExtArgs> _count?: boolean | Prisma.ProfileCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["profile"]> }, 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<{ export type ProfileSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean id?: boolean
userId?: boolean userId?: boolean
email?: boolean
firstName?: boolean firstName?: boolean
lastName?: boolean lastName?: boolean
phoneNumber?: boolean phoneNumber?: boolean
ghlContactId?: boolean ghlContactId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs> user?: boolean | Prisma.Profile$userArgs<ExtArgs>
}, ExtArgs["result"]["profile"]> }, ExtArgs["result"]["profile"]>
export type ProfileSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ export type ProfileSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean id?: boolean
userId?: boolean userId?: boolean
email?: boolean
firstName?: boolean firstName?: boolean
lastName?: boolean lastName?: boolean
phoneNumber?: boolean phoneNumber?: boolean
ghlContactId?: boolean ghlContactId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs> user?: boolean | Prisma.Profile$userArgs<ExtArgs>
}, ExtArgs["result"]["profile"]> }, ExtArgs["result"]["profile"]>
export type ProfileSelectScalar = { export type ProfileSelectScalar = {
id?: boolean id?: boolean
userId?: boolean userId?: boolean
email?: boolean
firstName?: boolean firstName?: boolean
lastName?: boolean lastName?: boolean
phoneNumber?: boolean phoneNumber?: boolean
ghlContactId?: 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> = { 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> addresses?: boolean | Prisma.Profile$addressesArgs<ExtArgs>
_count?: boolean | Prisma.ProfileCountOutputTypeDefaultArgs<ExtArgs> _count?: boolean | Prisma.ProfileCountOutputTypeDefaultArgs<ExtArgs>
} }
export type ProfileIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { 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> = { 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> = { export type $ProfilePayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "Profile" name: "Profile"
objects: { objects: {
user: Prisma.$UserPayload<ExtArgs> user: Prisma.$UserPayload<ExtArgs> | null
addresses: Prisma.$AddressPayload<ExtArgs>[] addresses: Prisma.$AddressPayload<ExtArgs>[]
} }
scalars: runtime.Types.Extensions.GetPayloadResult<{ scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string id: string
userId: string userId: string | null
email: string | null
firstName: string firstName: string
lastName: string lastName: string
phoneNumber: 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> { 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" 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> 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. * 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 { export interface ProfileFieldRefs {
readonly id: Prisma.FieldRef<"Profile", 'String'> readonly id: Prisma.FieldRef<"Profile", 'String'>
readonly userId: Prisma.FieldRef<"Profile", 'String'> readonly userId: Prisma.FieldRef<"Profile", 'String'>
readonly email: Prisma.FieldRef<"Profile", 'String'>
readonly firstName: Prisma.FieldRef<"Profile", 'String'> readonly firstName: Prisma.FieldRef<"Profile", 'String'>
readonly lastName: Prisma.FieldRef<"Profile", 'String'> readonly lastName: Prisma.FieldRef<"Profile", 'String'>
readonly phoneNumber: 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 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 * Profile.addresses
*/ */
File diff suppressed because it is too large Load Diff
+76 -53
View File
@@ -243,7 +243,7 @@ export type UserWhereInput = {
role?: Prisma.XOR<Prisma.UserRoleNullableScalarRelationFilter, Prisma.UserRoleWhereInput> | null role?: Prisma.XOR<Prisma.UserRoleNullableScalarRelationFilter, Prisma.UserRoleWhereInput> | null
sessions?: Prisma.SessionListRelationFilter sessions?: Prisma.SessionListRelationFilter
accounts?: Prisma.AccountListRelationFilter accounts?: Prisma.AccountListRelationFilter
profile?: Prisma.XOR<Prisma.ProfileNullableScalarRelationFilter, Prisma.ProfileWhereInput> | null profiles?: Prisma.ProfileListRelationFilter
licenseKeys?: Prisma.LicenseKeyListRelationFilter licenseKeys?: Prisma.LicenseKeyListRelationFilter
userProclamations?: Prisma.UserProclamationsListRelationFilter userProclamations?: Prisma.UserProclamationsListRelationFilter
} }
@@ -260,7 +260,7 @@ export type UserOrderByWithRelationInput = {
role?: Prisma.UserRoleOrderByWithRelationInput role?: Prisma.UserRoleOrderByWithRelationInput
sessions?: Prisma.SessionOrderByRelationAggregateInput sessions?: Prisma.SessionOrderByRelationAggregateInput
accounts?: Prisma.AccountOrderByRelationAggregateInput accounts?: Prisma.AccountOrderByRelationAggregateInput
profile?: Prisma.ProfileOrderByWithRelationInput profiles?: Prisma.ProfileOrderByRelationAggregateInput
licenseKeys?: Prisma.LicenseKeyOrderByRelationAggregateInput licenseKeys?: Prisma.LicenseKeyOrderByRelationAggregateInput
userProclamations?: Prisma.UserProclamationsOrderByRelationAggregateInput userProclamations?: Prisma.UserProclamationsOrderByRelationAggregateInput
} }
@@ -280,7 +280,7 @@ export type UserWhereUniqueInput = Prisma.AtLeast<{
role?: Prisma.XOR<Prisma.UserRoleNullableScalarRelationFilter, Prisma.UserRoleWhereInput> | null role?: Prisma.XOR<Prisma.UserRoleNullableScalarRelationFilter, Prisma.UserRoleWhereInput> | null
sessions?: Prisma.SessionListRelationFilter sessions?: Prisma.SessionListRelationFilter
accounts?: Prisma.AccountListRelationFilter accounts?: Prisma.AccountListRelationFilter
profile?: Prisma.XOR<Prisma.ProfileNullableScalarRelationFilter, Prisma.ProfileWhereInput> | null profiles?: Prisma.ProfileListRelationFilter
licenseKeys?: Prisma.LicenseKeyListRelationFilter licenseKeys?: Prisma.LicenseKeyListRelationFilter
userProclamations?: Prisma.UserProclamationsListRelationFilter userProclamations?: Prisma.UserProclamationsListRelationFilter
}, "id" | "email"> }, "id" | "email">
@@ -326,7 +326,7 @@ export type UserCreateInput = {
role?: Prisma.UserRoleCreateNestedOneWithoutUsersInput role?: Prisma.UserRoleCreateNestedOneWithoutUsersInput
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileCreateNestedOneWithoutUserInput profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput
} }
@@ -342,7 +342,7 @@ export type UserUncheckedCreateInput = {
roleId?: number | null roleId?: number | null
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileUncheckedCreateNestedOneWithoutUserInput profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput
} }
@@ -358,7 +358,7 @@ export type UserUpdateInput = {
role?: Prisma.UserRoleUpdateOneWithoutUsersNestedInput role?: Prisma.UserRoleUpdateOneWithoutUsersNestedInput
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUpdateOneWithoutUserNestedInput profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput
} }
@@ -374,7 +374,7 @@ export type UserUncheckedUpdateInput = {
roleId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null roleId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUncheckedUpdateOneWithoutUserNestedInput profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput
} }
@@ -452,6 +452,11 @@ export type UserSumOrderByAggregateInput = {
roleId?: Prisma.SortOrder roleId?: Prisma.SortOrder
} }
export type UserNullableScalarRelationFilter = {
is?: Prisma.UserWhereInput | null
isNot?: Prisma.UserWhereInput | null
}
export type UserScalarRelationFilter = { export type UserScalarRelationFilter = {
is?: Prisma.UserWhereInput is?: Prisma.UserWhereInput
isNot?: Prisma.UserWhereInput isNot?: Prisma.UserWhereInput
@@ -491,18 +496,20 @@ export type NullableIntFieldUpdateOperationsInput = {
divide?: number divide?: number
} }
export type UserCreateNestedOneWithoutProfileInput = { export type UserCreateNestedOneWithoutProfilesInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutProfileInput, Prisma.UserUncheckedCreateWithoutProfileInput> create?: Prisma.XOR<Prisma.UserCreateWithoutProfilesInput, Prisma.UserUncheckedCreateWithoutProfilesInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutProfileInput connectOrCreate?: Prisma.UserCreateOrConnectWithoutProfilesInput
connect?: Prisma.UserWhereUniqueInput connect?: Prisma.UserWhereUniqueInput
} }
export type UserUpdateOneRequiredWithoutProfileNestedInput = { export type UserUpdateOneWithoutProfilesNestedInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutProfileInput, Prisma.UserUncheckedCreateWithoutProfileInput> create?: Prisma.XOR<Prisma.UserCreateWithoutProfilesInput, Prisma.UserUncheckedCreateWithoutProfilesInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutProfileInput connectOrCreate?: Prisma.UserCreateOrConnectWithoutProfilesInput
upsert?: Prisma.UserUpsertWithoutProfileInput upsert?: Prisma.UserUpsertWithoutProfilesInput
disconnect?: Prisma.UserWhereInput | boolean
delete?: Prisma.UserWhereInput | boolean
connect?: Prisma.UserWhereUniqueInput 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 = { export type UserCreateNestedOneWithoutSessionsInput = {
@@ -595,15 +602,17 @@ export type UserCreateNestedOneWithoutLicenseKeysInput = {
connect?: Prisma.UserWhereUniqueInput connect?: Prisma.UserWhereUniqueInput
} }
export type UserUpdateOneRequiredWithoutLicenseKeysNestedInput = { export type UserUpdateOneWithoutLicenseKeysNestedInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutLicenseKeysInput, Prisma.UserUncheckedCreateWithoutLicenseKeysInput> create?: Prisma.XOR<Prisma.UserCreateWithoutLicenseKeysInput, Prisma.UserUncheckedCreateWithoutLicenseKeysInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutLicenseKeysInput connectOrCreate?: Prisma.UserCreateOrConnectWithoutLicenseKeysInput
upsert?: Prisma.UserUpsertWithoutLicenseKeysInput upsert?: Prisma.UserUpsertWithoutLicenseKeysInput
disconnect?: Prisma.UserWhereInput | boolean
delete?: Prisma.UserWhereInput | boolean
connect?: Prisma.UserWhereUniqueInput connect?: Prisma.UserWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutLicenseKeysInput, Prisma.UserUpdateWithoutLicenseKeysInput>, Prisma.UserUncheckedUpdateWithoutLicenseKeysInput> update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutLicenseKeysInput, Prisma.UserUpdateWithoutLicenseKeysInput>, Prisma.UserUncheckedUpdateWithoutLicenseKeysInput>
} }
export type UserCreateWithoutProfileInput = { export type UserCreateWithoutProfilesInput = {
id: string id: string
name: string name: string
email: string email: string
@@ -618,7 +627,7 @@ export type UserCreateWithoutProfileInput = {
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput
} }
export type UserUncheckedCreateWithoutProfileInput = { export type UserUncheckedCreateWithoutProfilesInput = {
id: string id: string
name: string name: string
email: string email: string
@@ -633,23 +642,23 @@ export type UserUncheckedCreateWithoutProfileInput = {
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput
} }
export type UserCreateOrConnectWithoutProfileInput = { export type UserCreateOrConnectWithoutProfilesInput = {
where: Prisma.UserWhereUniqueInput where: Prisma.UserWhereUniqueInput
create: Prisma.XOR<Prisma.UserCreateWithoutProfileInput, Prisma.UserUncheckedCreateWithoutProfileInput> create: Prisma.XOR<Prisma.UserCreateWithoutProfilesInput, Prisma.UserUncheckedCreateWithoutProfilesInput>
} }
export type UserUpsertWithoutProfileInput = { export type UserUpsertWithoutProfilesInput = {
update: Prisma.XOR<Prisma.UserUpdateWithoutProfileInput, Prisma.UserUncheckedUpdateWithoutProfileInput> update: Prisma.XOR<Prisma.UserUpdateWithoutProfilesInput, Prisma.UserUncheckedUpdateWithoutProfilesInput>
create: Prisma.XOR<Prisma.UserCreateWithoutProfileInput, Prisma.UserUncheckedCreateWithoutProfileInput> create: Prisma.XOR<Prisma.UserCreateWithoutProfilesInput, Prisma.UserUncheckedCreateWithoutProfilesInput>
where?: Prisma.UserWhereInput where?: Prisma.UserWhereInput
} }
export type UserUpdateToOneWithWhereWithoutProfileInput = { export type UserUpdateToOneWithWhereWithoutProfilesInput = {
where?: Prisma.UserWhereInput 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 id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.StringFieldUpdateOperationsInput | string
@@ -664,7 +673,7 @@ export type UserUpdateWithoutProfileInput = {
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput
} }
export type UserUncheckedUpdateWithoutProfileInput = { export type UserUncheckedUpdateWithoutProfilesInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.StringFieldUpdateOperationsInput | string
@@ -689,7 +698,7 @@ export type UserCreateWithoutSessionsInput = {
updatedAt?: Date | string updatedAt?: Date | string
role?: Prisma.UserRoleCreateNestedOneWithoutUsersInput role?: Prisma.UserRoleCreateNestedOneWithoutUsersInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileCreateNestedOneWithoutUserInput profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput
} }
@@ -704,7 +713,7 @@ export type UserUncheckedCreateWithoutSessionsInput = {
updatedAt?: Date | string updatedAt?: Date | string
roleId?: number | null roleId?: number | null
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileUncheckedCreateNestedOneWithoutUserInput profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput
} }
@@ -735,7 +744,7 @@ export type UserUpdateWithoutSessionsInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.UserRoleUpdateOneWithoutUsersNestedInput role?: Prisma.UserRoleUpdateOneWithoutUsersNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUpdateOneWithoutUserNestedInput profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput
} }
@@ -750,7 +759,7 @@ export type UserUncheckedUpdateWithoutSessionsInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
roleId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null roleId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUncheckedUpdateOneWithoutUserNestedInput profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput
} }
@@ -765,7 +774,7 @@ export type UserCreateWithoutAccountsInput = {
updatedAt?: Date | string updatedAt?: Date | string
role?: Prisma.UserRoleCreateNestedOneWithoutUsersInput role?: Prisma.UserRoleCreateNestedOneWithoutUsersInput
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileCreateNestedOneWithoutUserInput profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput
} }
@@ -780,7 +789,7 @@ export type UserUncheckedCreateWithoutAccountsInput = {
updatedAt?: Date | string updatedAt?: Date | string
roleId?: number | null roleId?: number | null
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileUncheckedCreateNestedOneWithoutUserInput profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput
} }
@@ -811,7 +820,7 @@ export type UserUpdateWithoutAccountsInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.UserRoleUpdateOneWithoutUsersNestedInput role?: Prisma.UserRoleUpdateOneWithoutUsersNestedInput
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUpdateOneWithoutUserNestedInput profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput
} }
@@ -826,7 +835,7 @@ export type UserUncheckedUpdateWithoutAccountsInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
roleId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null roleId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUncheckedUpdateOneWithoutUserNestedInput profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput
} }
@@ -841,7 +850,7 @@ export type UserCreateWithoutRoleInput = {
updatedAt?: Date | string updatedAt?: Date | string
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileCreateNestedOneWithoutUserInput profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput
} }
@@ -856,7 +865,7 @@ export type UserUncheckedCreateWithoutRoleInput = {
updatedAt?: Date | string updatedAt?: Date | string
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileUncheckedCreateNestedOneWithoutUserInput profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput
} }
@@ -912,7 +921,7 @@ export type UserCreateWithoutUserProclamationsInput = {
role?: Prisma.UserRoleCreateNestedOneWithoutUsersInput role?: Prisma.UserRoleCreateNestedOneWithoutUsersInput
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileCreateNestedOneWithoutUserInput profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput
} }
@@ -927,7 +936,7 @@ export type UserUncheckedCreateWithoutUserProclamationsInput = {
roleId?: number | null roleId?: number | null
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileUncheckedCreateNestedOneWithoutUserInput profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput
} }
@@ -958,7 +967,7 @@ export type UserUpdateWithoutUserProclamationsInput = {
role?: Prisma.UserRoleUpdateOneWithoutUsersNestedInput role?: Prisma.UserRoleUpdateOneWithoutUsersNestedInput
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUpdateOneWithoutUserNestedInput profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput
} }
@@ -973,7 +982,7 @@ export type UserUncheckedUpdateWithoutUserProclamationsInput = {
roleId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null roleId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUncheckedUpdateOneWithoutUserNestedInput profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput
} }
@@ -988,7 +997,7 @@ export type UserCreateWithoutLicenseKeysInput = {
role?: Prisma.UserRoleCreateNestedOneWithoutUsersInput role?: Prisma.UserRoleCreateNestedOneWithoutUsersInput
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileCreateNestedOneWithoutUserInput profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput
} }
@@ -1003,7 +1012,7 @@ export type UserUncheckedCreateWithoutLicenseKeysInput = {
roleId?: number | null roleId?: number | null
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileUncheckedCreateNestedOneWithoutUserInput profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput
} }
@@ -1034,7 +1043,7 @@ export type UserUpdateWithoutLicenseKeysInput = {
role?: Prisma.UserRoleUpdateOneWithoutUsersNestedInput role?: Prisma.UserRoleUpdateOneWithoutUsersNestedInput
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUpdateOneWithoutUserNestedInput profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput
} }
@@ -1049,7 +1058,7 @@ export type UserUncheckedUpdateWithoutLicenseKeysInput = {
roleId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null roleId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUncheckedUpdateOneWithoutUserNestedInput profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput
} }
@@ -1073,7 +1082,7 @@ export type UserUpdateWithoutRoleInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUpdateOneWithoutUserNestedInput profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput
} }
@@ -1088,7 +1097,7 @@ export type UserUncheckedUpdateWithoutRoleInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUncheckedUpdateOneWithoutUserNestedInput profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput
} }
@@ -1111,6 +1120,7 @@ export type UserUncheckedUpdateManyWithoutRoleInput = {
export type UserCountOutputType = { export type UserCountOutputType = {
sessions: number sessions: number
accounts: number accounts: number
profiles: number
licenseKeys: number licenseKeys: number
userProclamations: number userProclamations: number
} }
@@ -1118,6 +1128,7 @@ export type UserCountOutputType = {
export type UserCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type UserCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
sessions?: boolean | UserCountOutputTypeCountSessionsArgs sessions?: boolean | UserCountOutputTypeCountSessionsArgs
accounts?: boolean | UserCountOutputTypeCountAccountsArgs accounts?: boolean | UserCountOutputTypeCountAccountsArgs
profiles?: boolean | UserCountOutputTypeCountProfilesArgs
licenseKeys?: boolean | UserCountOutputTypeCountLicenseKeysArgs licenseKeys?: boolean | UserCountOutputTypeCountLicenseKeysArgs
userProclamations?: boolean | UserCountOutputTypeCountUserProclamationsArgs userProclamations?: boolean | UserCountOutputTypeCountUserProclamationsArgs
} }
@@ -1146,6 +1157,13 @@ export type UserCountOutputTypeCountAccountsArgs<ExtArgs extends runtime.Types.E
where?: Prisma.AccountWhereInput 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 * UserCountOutputType without action
*/ */
@@ -1173,7 +1191,7 @@ export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
role?: boolean | Prisma.User$roleArgs<ExtArgs> role?: boolean | Prisma.User$roleArgs<ExtArgs>
sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs> sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs>
accounts?: boolean | Prisma.User$accountsArgs<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> licenseKeys?: boolean | Prisma.User$licenseKeysArgs<ExtArgs>
userProclamations?: boolean | Prisma.User$userProclamationsArgs<ExtArgs> userProclamations?: boolean | Prisma.User$userProclamationsArgs<ExtArgs>
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<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> role?: boolean | Prisma.User$roleArgs<ExtArgs>
sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs> sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs>
accounts?: boolean | Prisma.User$accountsArgs<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> licenseKeys?: boolean | Prisma.User$licenseKeysArgs<ExtArgs>
userProclamations?: boolean | Prisma.User$userProclamationsArgs<ExtArgs> userProclamations?: boolean | Prisma.User$userProclamationsArgs<ExtArgs>
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs> _count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
@@ -1237,7 +1255,7 @@ export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
role: Prisma.$UserRolePayload<ExtArgs> | null role: Prisma.$UserRolePayload<ExtArgs> | null
sessions: Prisma.$SessionPayload<ExtArgs>[] sessions: Prisma.$SessionPayload<ExtArgs>[]
accounts: Prisma.$AccountPayload<ExtArgs>[] accounts: Prisma.$AccountPayload<ExtArgs>[]
profile: Prisma.$ProfilePayload<ExtArgs> | null profiles: Prisma.$ProfilePayload<ExtArgs>[]
licenseKeys: Prisma.$LicenseKeyPayload<ExtArgs>[] licenseKeys: Prisma.$LicenseKeyPayload<ExtArgs>[]
userProclamations: Prisma.$UserProclamationsPayload<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> 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> 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> 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> 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> 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 * 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 include?: Prisma.ProfileInclude<ExtArgs> | null
where?: Prisma.ProfileWhereInput where?: Prisma.ProfileWhereInput
orderBy?: Prisma.ProfileOrderByWithRelationInput | Prisma.ProfileOrderByWithRelationInput[]
cursor?: Prisma.ProfileWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.ProfileScalarFieldEnum | Prisma.ProfileScalarFieldEnum[]
} }
/** /**
+22 -47
View File
@@ -5,9 +5,9 @@ import {
canonicalIdForFrequency, canonicalIdForFrequency,
serializeCategory, serializeCategory,
serializeColorPalette, serializeColorPalette,
serializeGroup,
serializeLicense, serializeLicense,
serializeProclamation, serializeProclamation
serializeSubscriptionLevel
} from './v2-serializers'; } from './v2-serializers';
describe('canonicalIdForFrequency', () => { describe('canonicalIdForFrequency', () => {
@@ -61,7 +61,7 @@ describe('serializeProclamation', () => {
name: 'Peace & Relaxation', name: 'Peace & Relaxation',
content: '<p>The text content, HTML formatted</p>', content: '<p>The text content, HTML formatted</p>',
categoryId: 5, categoryId: 5,
subscriptionLevelId: 1 groupId: 1
}; };
it('derives solfeggioFrequencyId from the Hz value, not the DB id', () => { it('derives solfeggioFrequencyId from the Hz value, not the DB id', () => {
@@ -84,13 +84,13 @@ describe('serializeProclamation', () => {
expect(record).toBeNull(); expect(record).toBeNull();
}); });
it('passes through a null subscriptionLevelId (kiosk treats as level 1)', () => { it('passes through categoryId and groupId unchanged', () => {
const record = serializeProclamation({ const record = serializeProclamation({
...base, ...base,
subscriptionLevelId: null,
solfeggioFrequency: solarPlexus528 solfeggioFrequency: solarPlexus528
}); });
expect(record?.subscriptionLevelId).toBeNull(); expect(record?.categoryId).toBe(5);
expect(record?.groupId).toBe(1);
}); });
it('embeds a 7-color palette when provided', () => { it('embeds a 7-color palette when provided', () => {
@@ -116,64 +116,39 @@ describe('serializeProclamation', () => {
}); });
describe('serializeCategory', () => { describe('serializeCategory', () => {
it('keeps parentId null for root categories', () => { it('maps rows to a flat {id, name, sortOrder} with no parentId', () => {
expect(serializeCategory({ id: 4, name: 'Spiritual', sortOrder: 0, parentId: null })).toEqual({ expect(serializeCategory({ id: 4, name: 'Spiritual', sortOrder: 0 })).toEqual({
id: 4, id: 4,
name: 'Spiritual', name: 'Spiritual',
sortOrder: 0, sortOrder: 0
parentId: null
}); });
expect(
Object.keys(serializeCategory({ id: 4, name: 'Spiritual', sortOrder: 0 }))
).not.toContain('parentId');
}); });
});
it('keeps parentId int for children', () => { describe('serializeGroup', () => {
expect(serializeCategory({ id: 5, name: 'General', sortOrder: 0, parentId: 4 })).toEqual({ it('maps rows to {id, name, sortOrder}', () => {
id: 5, expect(serializeGroup({ id: 1, name: 'General', sortOrder: 0 })).toEqual({
id: 1,
name: 'General', name: 'General',
sortOrder: 0, sortOrder: 0
parentId: 4
}); });
}); });
}); });
describe('serializeLicense', () => { 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({ const result = serializeLicense({
key: 'XXXX-XXXX-XXXX-XXXX', key: 'XXXX-XXXX-XXXX-XXXX',
emailAddress: 'user@example.com', emailAddress: 'user@example.com'
levelName: 'Advanced',
level: { id: 2, name: 'Advanced', description: 'General + Specific' }
}); });
expect(result).toEqual({ expect(result).toEqual({
key: 'XXXX-XXXX-XXXX-XXXX', key: 'XXXX-XXXX-XXXX-XXXX',
email_address: 'user@example.com', 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'
}); });
expect(Object.keys(result)).not.toContain('subscription_level');
}); });
}); });
+14 -35
View File
@@ -55,8 +55,8 @@ export type V2Proclamation = {
name: string; name: string;
content: string; content: string;
categoryId: number; categoryId: number;
groupId: number;
solfeggioFrequencyId: number; solfeggioFrequencyId: number;
subscriptionLevelId: number | null;
colorPalette?: string[]; colorPalette?: string[];
}; };
@@ -65,7 +65,7 @@ export type ProclamationWithFrequency = {
name: string; name: string;
content: string; content: string;
categoryId: number; categoryId: number;
subscriptionLevelId: number | null; groupId: number;
solfeggioFrequency: { value: number }; solfeggioFrequency: { value: number };
}; };
@@ -83,8 +83,8 @@ export function serializeProclamation(
name: p.name, name: p.name,
content: p.content, content: p.content,
categoryId: p.categoryId, categoryId: p.categoryId,
solfeggioFrequencyId, groupId: p.groupId,
subscriptionLevelId: p.subscriptionLevelId solfeggioFrequencyId
}; };
if (palette && palette.length > 0) { if (palette && palette.length > 0) {
@@ -98,53 +98,32 @@ export type V2Category = {
id: number; id: number;
name: string; name: string;
sortOrder: number; 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; id: number;
name: string; name: string;
sortOrder: number; 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: { export function serializeGroup(g: { id: number; name: string; sortOrder: number }): V2Group {
id: number; return { id: g.id, name: g.name, sortOrder: g.sortOrder };
name: string;
description: string | null;
}): V2SubscriptionLevel {
return { id: level.id, name: level.name, description: level.description };
} }
export type V2License = { export type V2License = {
key: string; key: string;
email_address: 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; /** Build the POST /license response. */
* when no SubscriptionLevel row matches the license's level name, fall back to export function serializeLicense(opts: { key: string; emailAddress: string }): V2License {
* 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 {
return { return {
key: opts.key, key: opts.key,
email_address: opts.emailAddress, 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 }
}; };
} }
+1 -1
View File
@@ -17,7 +17,7 @@ function signedRequest(opts: {
const path = '/api/v2/automations/create/account'; const path = '/api/v2/automations/create/account';
const timestamp = String(Math.floor((opts.now ?? new Date()).getTime() / 1000)); const timestamp = String(Math.floor((opts.now ?? new Date()).getTime() / 1000));
const nonce = 'a48c0f2e-5f34-4b0b-9a12-3c7d6b8e1f00'; 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 ts = timestamp;
let nonceValue = nonce; let nonceValue = nonce;
+23
View File
@@ -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 };
}
-391
View File
@@ -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 };
}
}
```
-227
View File
@@ -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);
});
});
-190
View File
@@ -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 };
}
-46
View File
@@ -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;
}
-7
View File
@@ -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';
-15
View File
@@ -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;
}
-26
View File
@@ -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;
}
-290
View File
@@ -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)
}));
}
-15
View File
@@ -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);
}
-74
View File
@@ -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()
});
-47
View File
@@ -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
View File
@@ -51,6 +51,7 @@ export function mockUserWithProfile(overrides?: Record<string, unknown>) {
lastName: 'Doe', lastName: 'Doe',
phoneNumber: '555-0100' phoneNumber: '555-0100'
}, },
profiles: [{ id: 'profile-1', userId: 'user-1' }],
...overrides ...overrides
}); });
} }
@@ -76,6 +77,7 @@ export function mockUserWithProfileAndAddresses(overrides?: Record<string, unkno
} }
] ]
}, },
profiles: [{ id: 'profile-1', userId: 'user-1' }],
...overrides ...overrides
}); });
} }
@@ -100,6 +102,7 @@ export function mockProclamation(overrides?: Record<string, unknown>) {
content: '<p>Test content</p>', content: '<p>Test content</p>',
solfeggioFrequencyId: 1, solfeggioFrequencyId: 1,
categoryId: 1, categoryId: 1,
groupId: 1,
price: 0.99, price: 0.99,
solfeggioFrequency: { solfeggioFrequency: {
id: 1, id: 1,
@@ -108,9 +111,7 @@ export function mockProclamation(overrides?: Record<string, unknown>) {
}, },
category: { category: {
id: 1, id: 1,
name: 'Healing', name: 'Healing'
parentId: null,
parent: null
}, },
...overrides ...overrides
}; };
@@ -121,7 +122,16 @@ export function mockCategory(overrides?: Record<string, unknown>) {
return { return {
id: 1, id: 1,
name: 'Healing', name: 'Healing',
parentId: null, ...overrides
};
}
/** Minimal proclamation group. */
export function mockGroup(overrides?: Record<string, unknown>) {
return {
id: 1,
name: 'General',
sortOrder: 0,
...overrides ...overrides
}; };
} }
@@ -8,20 +8,18 @@ export const load: PageServerLoad = async ({ locals }) => {
throw error(403, 'not authorized'); throw error(403, 'not authorized');
} }
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({ where: { id: locals.user.id } });
where: { id: locals.user.id },
include: {
profile: {
include: { addresses: true }
}
}
});
if (!user) { if (!user) {
throw error(403, 'not authorized'); 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 = { export const actions: Actions = {
@@ -40,7 +38,7 @@ export const actions: Actions = {
} }
try { try {
await prisma.profile.update({ await prisma.profile.updateMany({
where: { userId: locals.user.id }, where: { userId: locals.user.id },
data: { firstName, lastName, phoneNumber } data: { firstName, lastName, phoneNumber }
}); });
@@ -67,7 +65,7 @@ export const actions: Actions = {
return fail(400, { error: 'All address fields are required.', action: 'addAddress' }); 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 }, where: { userId: locals.user.id },
select: { id: true } select: { id: true }
}); });
@@ -23,67 +23,58 @@
} }
</script> </script>
<div class="flex h-full min-h-0 flex-col gap-8 p-6 dark:text-gray-100"> <div class="mx-auto w-full max-w-3xl">
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">Account</h1> <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) --> <!-- Email (read-only) -->
<div> <div class="mt-6 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6">
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">Email</p> <p class="text-xs tracking-wide text-slate-500 uppercase">Email</p>
<p class="mt-1 text-gray-900 dark:text-gray-100">{data.user.email}</p> <p class="mt-1 text-base break-words text-slate-900">{data.user.email}</p>
</div> </div>
<!-- Profile Form --> <!-- Profile Form -->
<section> <section class="mt-6 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6">
<h2 class="mb-4 text-lg font-medium text-gray-900 dark:text-gray-100">Profile</h2> <h2 class="text-lg font-semibold text-slate-900">Profile</h2>
{#if form?.action === 'updateProfile' && form?.success} {#if form?.action === 'updateProfile' && form?.success}
<div <div class="mt-4 rounded-lg bg-green-50 px-4 py-3 text-sm text-green-800" role="status">
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"
>
Profile updated successfully. Profile updated successfully.
</div> </div>
{/if} {/if}
{#if form?.action === 'updateProfile' && form?.error} {#if form?.action === 'updateProfile' && form?.error}
<div <div class="mt-4 rounded-lg bg-red-50 px-4 py-3 text-sm text-red-700" role="alert">
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"
>
{form.error} {form.error}
</div> </div>
{/if} {/if}
<form method="POST" action="?/updateProfile" class="flex max-w-lg flex-col gap-4"> <form method="POST" action="?/updateProfile" class="mt-5 flex flex-col gap-4">
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div> <div class="flex flex-col gap-1.5">
<label for="firstName" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="firstName" class="text-sm font-medium text-slate-700"> First Name </label>
First Name
</label>
<input <input
type="text" type="text"
id="firstName" id="firstName"
name="firstName" name="firstName"
value={data.user.profile?.firstName ?? ''} value={data.user.profile?.firstName ?? ''}
required 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> <div class="flex flex-col gap-1.5">
<label for="lastName" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="lastName" class="text-sm font-medium text-slate-700"> Last Name </label>
Last Name
</label>
<input <input
type="text" type="text"
id="lastName" id="lastName"
name="lastName" name="lastName"
value={data.user.profile?.lastName ?? ''} value={data.user.profile?.lastName ?? ''}
required 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> </div>
<div> <div class="flex flex-col gap-1.5">
<label for="phoneNumber" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="phoneNumber" class="text-sm font-medium text-slate-700"> Phone Number </label>
Phone Number
</label>
<input <input
type="tel" type="tel"
id="phoneNumber" id="phoneNumber"
@@ -91,13 +82,13 @@
bind:value={phoneNumber} bind:value={phoneNumber}
oninput={handlePhoneInput} oninput={handlePhoneInput}
required 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>
<div> <div>
<button <button
type="submit" 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 Save Profile
</button> </button>
@@ -106,138 +97,112 @@
</section> </section>
<!-- Addresses --> <!-- Addresses -->
<section> <section class="mt-6 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6">
<div class="mb-4 flex items-center justify-between"> <div class="flex items-center justify-between gap-4">
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">Addresses</h2> <h2 class="text-lg font-semibold text-slate-900">Addresses</h2>
<button <button
type="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)} 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'} {showAddressForm ? 'Cancel' : 'Add Address'}
</button> </button>
</div> </div>
{#if form?.action === 'addAddress' && form?.success} {#if form?.action === 'addAddress' && form?.success}
<div <div class="mt-4 rounded-lg bg-green-50 px-4 py-3 text-sm text-green-800" role="status">
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"
>
Address added successfully. Address added successfully.
</div> </div>
{/if} {/if}
{#if form?.action === 'addAddress' && form?.error} {#if form?.action === 'addAddress' && form?.error}
<div <div class="mt-4 rounded-lg bg-red-50 px-4 py-3 text-sm text-red-700" role="alert">
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"
>
{form.error} {form.error}
</div> </div>
{/if} {/if}
{#if showAddressForm} {#if showAddressForm}
<form <form method="POST" action="?/addAddress" class="mt-5 flex flex-col gap-4">
method="POST" <div class="flex flex-col gap-1.5">
action="?/addAddress" <label for="street" class="text-sm font-medium text-slate-700">Street</label>
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" <input
> type="text"
<div class="flex flex-col gap-3"> id="street"
<div> name="street"
<label for="street" class="block text-sm font-medium text-gray-700 dark:text-gray-300" required
>Street</label 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 <input
type="text" type="text"
id="street" id="city"
name="street" name="city"
required 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 class="grid grid-cols-2 gap-3"> <div class="flex flex-col gap-1.5">
<div> <label for="state" class="text-sm font-medium text-slate-700">State</label>
<label for="city" class="block text-sm font-medium text-gray-700 dark:text-gray-300" <input
>City</label type="text"
> id="state"
<input name="state"
type="text" required
id="city" 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"
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> </div>
<div class="grid grid-cols-2 gap-3"> </div>
<div> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<label <div class="flex flex-col gap-1.5">
for="postalCode" <label for="postalCode" class="text-sm font-medium text-slate-700"> Postal Code </label>
class="block text-sm font-medium text-gray-700 dark:text-gray-300" <input
> type="text"
Postal Code id="postalCode"
</label> name="postalCode"
<input required
type="text" 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"
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>
<div> <div class="flex flex-col gap-1.5">
<button <label for="country" class="text-sm font-medium text-slate-700">Country</label>
type="submit" <input
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" type="text"
> id="country"
Save Address name="country"
</button> 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> </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> </form>
{/if} {/if}
{#if data.user.profile?.addresses && data.user.profile.addresses.length > 0} {#if data.user.profile?.addresses && data.user.profile.addresses.length > 0}
<div class="flex flex-col gap-3"> <div class="mt-5 grid grid-cols-1 gap-4 sm:grid-cols-2">
{#each data.user.profile.addresses as address} {#each data.user.profile.addresses as address (address.id)}
<div <div class="rounded-lg border border-slate-200 bg-slate-50 p-4">
class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-900" <p class="text-sm font-medium text-slate-900">{address.street}</p>
> <p class="mt-1 text-sm text-slate-600">
<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">
{address.city}, {address.state} {address.city}, {address.state}
{address.postalCode} {address.postalCode}
</p> </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> </div>
{/each} {/each}
</div> </div>
{:else if !showAddressForm} {: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} {/if}
</section> </section>
</div> </div>
@@ -10,7 +10,7 @@ import {
vi.mock('$lib/db', () => ({ vi.mock('$lib/db', () => ({
prisma: { prisma: {
user: { findUnique: vi.fn() }, user: { findUnique: vi.fn() },
profile: { update: vi.fn(), findUnique: vi.fn() }, profile: { findFirst: vi.fn(), updateMany: vi.fn() },
address: { create: vi.fn() } address: { create: vi.fn() }
} }
})); }));
@@ -19,8 +19,8 @@ import { prisma } from '$lib/db';
import { load, actions } from './+page.server'; import { load, actions } from './+page.server';
const userFindUnique = prisma.user.findUnique as ReturnType<typeof vi.fn>; const userFindUnique = prisma.user.findUnique as ReturnType<typeof vi.fn>;
const profileUpdate = prisma.profile.update as ReturnType<typeof vi.fn>; const profileFindFirst = prisma.profile.findFirst as ReturnType<typeof vi.fn>;
const profileFindUnique = prisma.profile.findUnique 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 addressCreate = prisma.address.create as ReturnType<typeof vi.fn>;
const invokeAction = (name: string, request: Request, locals: App.Locals) => 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', () => { describe('(protected)/account page server', () => {
beforeEach(() => { beforeEach(() => {
userFindUnique.mockReset(); userFindUnique.mockReset();
profileUpdate.mockReset(); profileFindFirst.mockReset();
profileFindUnique.mockReset(); profileUpdateMany.mockReset();
addressCreate.mockReset(); addressCreate.mockReset();
}); });
@@ -51,24 +51,26 @@ describe('(protected)/account page server', () => {
}); });
it('returns user with profile and addresses', async () => { it('returns user with profile and addresses', async () => {
const user = mockUserWithProfileAndAddresses(); const { profile, ...user } = mockUserWithProfileAndAddresses() as any;
userFindUnique.mockResolvedValue(user); userFindUnique.mockResolvedValue(user);
profileFindFirst.mockResolvedValue(profile);
const locals = createLocals(mockUser() as any); const locals = createLocals(mockUser() as any);
const result = (await load({ locals } as any)) 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); expect(result.user.profile.addresses).toHaveLength(1);
}); });
it('includes addresses in the query', async () => { it('loads the profile with addresses by user id', async () => {
userFindUnique.mockResolvedValue(mockUserWithProfileAndAddresses()); userFindUnique.mockResolvedValue(mockUser());
profileFindFirst.mockResolvedValue({ id: 'profile-1' });
const locals = createLocals(mockUser() as any); const locals = createLocals(mockUser() as any);
await load({ locals } as any); await load({ locals } as any);
expect(userFindUnique).toHaveBeenCalledWith({ expect(profileFindFirst).toHaveBeenCalledWith({
where: { id: 'user-1' }, where: { userId: 'user-1' },
include: { profile: { include: { addresses: true } } } include: { addresses: true }
}); });
}); });
}); });
@@ -125,7 +127,7 @@ describe('(protected)/account page server', () => {
}); });
it('updates profile with server-formatted phone number on success', async () => { it('updates profile with server-formatted phone number on success', async () => {
profileUpdate.mockResolvedValue({}); profileUpdateMany.mockResolvedValue({});
const request = createFormRequest({ const request = createFormRequest({
firstName: 'Jane', firstName: 'Jane',
@@ -136,14 +138,14 @@ describe('(protected)/account page server', () => {
const locals = createLocals(mockUser() as any); const locals = createLocals(mockUser() as any);
const result = await invokeAction('updateProfile', request, locals); const result = await invokeAction('updateProfile', request, locals);
expect(result).toEqual({ success: true, action: 'updateProfile' }); expect(result).toEqual({ success: true, action: 'updateProfile' });
expect(profileUpdate).toHaveBeenCalledWith({ expect(profileUpdateMany).toHaveBeenCalledWith({
where: { userId: 'user-1' }, where: { userId: 'user-1' },
data: { firstName: 'Jane', lastName: 'Smith', phoneNumber: '8175551234' } data: { firstName: 'Jane', lastName: 'Smith', phoneNumber: '8175551234' }
}); });
}); });
it('returns fail(500) when prisma update throws', async () => { it('returns fail(500) when prisma update throws', async () => {
profileUpdate.mockRejectedValue(new Error('db error')); profileUpdateMany.mockRejectedValue(new Error('db error'));
const request = createFormRequest({ const request = createFormRequest({
firstName: 'Jane', firstName: 'Jane',
@@ -190,7 +192,7 @@ describe('(protected)/account page server', () => {
}); });
it('throws 400 when profile is not found', async () => { it('throws 400 when profile is not found', async () => {
profileFindUnique.mockResolvedValue(null); profileFindFirst.mockResolvedValue(null);
const request = createFormRequest({ const request = createFormRequest({
street: '123 Main St', street: '123 Main St',
@@ -208,7 +210,7 @@ describe('(protected)/account page server', () => {
}); });
it('creates address linked to profile on success', async () => { it('creates address linked to profile on success', async () => {
profileFindUnique.mockResolvedValue({ id: 'profile-1' }); profileFindFirst.mockResolvedValue({ id: 'profile-1' });
addressCreate.mockResolvedValue({}); addressCreate.mockResolvedValue({});
const request = createFormRequest({ const request = createFormRequest({
@@ -235,7 +237,7 @@ describe('(protected)/account page server', () => {
}); });
it('returns fail(500) when prisma create throws', async () => { 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')); addressCreate.mockRejectedValue(new Error('db error'));
const request = createFormRequest({ const request = createFormRequest({
@@ -42,41 +42,74 @@
</script> </script>
<Window> <Window>
<form method="POST" class="border p-6 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100"> <form method="POST" class="flex flex-col items-center gap-5">
<p class="mx-auto my-4 text-center italic">Please finish setting up your profile</p> <h1 class="text-center text-2xl font-bold text-slate-900">
Please finish setting up your profile
</h1>
{#if data.user?.image} {#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} {: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} {/if}
<p class="mx-auto my-4 text-center">{data.user?.email}</p>
<div class="flex flex-col"> <p class="text-base text-slate-900">{data.user?.email}</p>
<input type="text" name="userId" value={data.user?.id} class="hidden" />
<input <div class="flex w-full flex-col gap-4">
type="text" <input type="hidden" name="userId" value={data.user?.id} />
name="firstName"
placeholder="First Name" <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
class="my-2" <div class="flex flex-col gap-1.5">
bind:value={firstName} <label for="firstName" class="text-sm font-medium text-slate-700"> First Name </label>
/> <input
<input id="firstName"
type="text" type="text"
name="lastName" name="firstName"
placeholder="Last Name" placeholder="First Name"
class="my-2" bind:value={firstName}
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"
/> />
<input </div>
type="tel" <div class="flex flex-col gap-1.5">
name="phoneNumber" <label for="lastName" class="text-sm font-medium text-slate-700"> Last Name </label>
placeholder="Phone Number" <input
class="my-2" id="lastName"
bind:value={phoneNumber} type="text"
oninput={handlePhoneInput} 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> </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> </form>
</Window> </Window>
@@ -7,19 +7,16 @@ export const load: PageServerLoad = async ({ locals }) => {
throw error(403, 'not authorized'); throw error(403, 'not authorized');
} }
const profile = await prisma.profile.findUnique({ const profile = await prisma.profile.findFirst({
where: { userId: locals.user.id }, where: { userId: locals.user.id },
select: { firstName: true } select: { firstName: true }
}); });
const [keyStats, proclamationCount] = await Promise.all([ const keyStats = await prisma.licenseKey.groupBy({
prisma.licenseKey.groupBy({ by: ['active'],
by: ['active'], where: { userId: locals.user.id },
where: { userId: locals.user.id }, _count: true
_count: true });
}),
prisma.proclamation.count()
]);
const activeKeys = keyStats.find((s) => s.active)?._count ?? 0; const activeKeys = keyStats.find((s) => s.active)?._count ?? 0;
const totalKeys = keyStats.reduce((sum, s) => sum + s._count, 0); const totalKeys = keyStats.reduce((sum, s) => sum + s._count, 0);
@@ -27,7 +24,6 @@ export const load: PageServerLoad = async ({ locals }) => {
return { return {
firstName: profile?.firstName ?? 'User', firstName: profile?.firstName ?? 'User',
activeKeys, activeKeys,
totalKeys, totalKeys
proclamationCount
}; };
}; };
@@ -1,42 +1,33 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
let { data }: PageProps = $props(); let { data }: PageProps = $props();
</script> </script>
<div class="flex h-full min-h-0 flex-col gap-6 p-6 dark:text-gray-100"> <div>
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100"> <h1 class="text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl">
Welcome, {data.firstName}! Welcome, {data.firstName}!
</h1> </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 <a
href="/license-keys" href={resolve('/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" 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> <span class="text-sm font-medium text-slate-600">Active License Keys</span>
<p class="mt-2 text-3xl font-bold text-gray-900 dark:text-gray-100">{data.activeKeys}</p> <span class="text-3xl font-bold text-indigo-600">{data.activeKeys}</span>
<p class="mt-1 text-sm text-gray-400 dark:text-gray-500">{data.totalKeys} total</p> <span class="text-sm text-slate-500">{data.totalKeys} total</span>
</a> </a>
<a <a
href="/admin/proclamations" href={resolve('/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" 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> <span class="text-sm font-medium text-slate-600">Account</span>
<p class="mt-2 text-3xl font-bold text-gray-900 dark:text-gray-100"> <span class="text-2xl font-semibold text-slate-900">Manage Profile</span>
{data.proclamationCount} <span class="text-sm text-slate-500">Edit your details</span>
</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>
</a> </a>
</div> </div>
</div> </div>
@@ -4,24 +4,21 @@ import { createLocals, mockUser } from '$lib/test-utils';
vi.mock('$lib/db', () => ({ vi.mock('$lib/db', () => ({
prisma: { prisma: {
profile: { findUnique: vi.fn() }, profile: { findFirst: vi.fn() },
licenseKey: { groupBy: vi.fn() }, licenseKey: { groupBy: vi.fn() }
proclamation: { count: vi.fn() }
} }
})); }));
import { prisma } from '$lib/db'; import { prisma } from '$lib/db';
import { load } from './+page.server'; 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 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', () => { describe('(protected)/dashboard page server load', () => {
beforeEach(() => { beforeEach(() => {
profileFindUnique.mockReset(); profileFindFirst.mockReset();
licenseKeyGroupBy.mockReset(); licenseKeyGroupBy.mockReset();
proclamationCount.mockReset();
}); });
it('throws 403 when locals.user is undefined', async () => { 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 () => { it('returns firstName from profile', async () => {
profileFindUnique.mockResolvedValue({ firstName: 'Jane' }); profileFindFirst.mockResolvedValue({ firstName: 'Jane' });
licenseKeyGroupBy.mockResolvedValue([]); licenseKeyGroupBy.mockResolvedValue([]);
proclamationCount.mockResolvedValue(0);
const locals = createLocals(mockUser() as any); const locals = createLocals(mockUser() as any);
const result = (await load({ locals } as any)) 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 () => { it('defaults firstName to "User" when profile is null', async () => {
profileFindUnique.mockResolvedValue(null); profileFindFirst.mockResolvedValue(null);
licenseKeyGroupBy.mockResolvedValue([]); licenseKeyGroupBy.mockResolvedValue([]);
proclamationCount.mockResolvedValue(0);
const locals = createLocals(mockUser() as any); const locals = createLocals(mockUser() as any);
const result = (await load({ locals } as any)) 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 () => { it('computes activeKeys and totalKeys from groupBy results', async () => {
profileFindUnique.mockResolvedValue({ firstName: 'Jane' }); profileFindFirst.mockResolvedValue({ firstName: 'Jane' });
licenseKeyGroupBy.mockResolvedValue([ licenseKeyGroupBy.mockResolvedValue([
{ active: true, _count: 3 }, { active: true, _count: 3 },
{ active: false, _count: 2 } { active: false, _count: 2 }
]); ]);
proclamationCount.mockResolvedValue(10);
const locals = createLocals(mockUser() as any); const locals = createLocals(mockUser() as any);
const result = (await load({ locals } as any)) 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 () => { it('returns 0 for activeKeys when none are active', async () => {
profileFindUnique.mockResolvedValue({ firstName: 'Jane' }); profileFindFirst.mockResolvedValue({ firstName: 'Jane' });
licenseKeyGroupBy.mockResolvedValue([{ active: false, _count: 4 }]); licenseKeyGroupBy.mockResolvedValue([{ active: false, _count: 4 }]);
proclamationCount.mockResolvedValue(0);
const locals = createLocals(mockUser() as any); const locals = createLocals(mockUser() as any);
const result = (await load({ locals } as any)) as any; const result = (await load({ locals } as any)) as any;
expect(result.activeKeys).toBe(0); expect(result.activeKeys).toBe(0);
expect(result.totalKeys).toBe(4); 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(); let { data }: PageProps = $props();
</script> </script>
<div class="flex h-full min-h-0 flex-col gap-6 p-6 dark:text-gray-100"> <div>
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">License Keys</h1> <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} {#if data.licenseKeys.length > 0}
<div class="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700"> <div class="mt-6 overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> <!-- Desktop / tablet: table -->
<thead class="bg-gray-50 dark:bg-gray-800"> <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> <tr>
<th <th class="px-6 py-3 font-semibold">Key</th>
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase dark:text-gray-400" <th class="px-6 py-3 font-semibold">Status</th>
>
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>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-900"> <tbody class="divide-y divide-slate-100">
{#each data.licenseKeys as key} {#each data.licenseKeys as key (key.key)}
<tr> <tr class="hover:bg-slate-50">
<td <td class="px-6 py-4 font-mono text-slate-800">{key.key}</td>
class="px-6 py-4 font-mono text-sm whitespace-nowrap text-gray-900 dark:text-gray-100" <td class="px-6 py-4">
>
{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">
<span <span
class="inline-flex rounded-full px-2 text-xs leading-5 font-semibold {key.active class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium {key.active
? 'bg-green-100 text-green-800 dark:bg-gray-700 dark:text-gray-200' ? 'bg-green-100 text-green-700'
: 'bg-red-100 text-red-800 dark:bg-gray-700 dark:text-gray-200'}" : 'bg-slate-100 text-slate-600'}"
> >
{key.active ? 'Active' : 'Inactive'} {key.active ? 'Active' : 'Inactive'}
</span> </span>
@@ -53,10 +41,26 @@
{/each} {/each}
</tbody> </tbody>
</table> </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> </div>
{:else} {:else}
<div class="flex flex-1 items-center justify-center"> <div class="mt-6 rounded-2xl border border-dashed border-slate-300 bg-white p-8 text-center">
<p class="text-lg text-gray-500 dark:text-gray-400">You don't have any license keys yet.</p> <p class="text-sm text-slate-600">You don't have any license keys yet.</p>
</div> </div>
{/if} {/if}
</div> </div>
+2 -2
View File
@@ -9,12 +9,12 @@ export const load: LayoutServerLoad = async ({ locals, url }) => {
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { id: locals.user.id }, 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) { if (!user) {
throw error(403, 'not authorized'); 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 // Skip redirect when already on /account/setup to avoid redirect loop
throw redirect(303, '/account/setup'); throw redirect(303, '/account/setup');
} }
+5 -68
View File
@@ -1,74 +1,11 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state'; import type { Snippet } from 'svelte';
import { canAccessAdmin } from '$lib/svelte-admin/roles';
import { getAllModelConfigs } from '$lib/svelte-admin/registry';
import type { LayoutData } from './$types';
let { children, data } = $props<{ children: any; data: LayoutData }>(); let { children }: { children: Snippet } = $props();
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 + '/');
}
</script> </script>
<div class="flex min-h-0 flex-1"> <div class="min-h-screen bg-slate-50 text-slate-900">
<!-- Sidebar --> <main class="mx-auto w-full max-w-6xl px-4 py-6 sm:px-6 sm:py-8">
<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">
{@render children()} {@render children()}
</div> </main>
</div> </div>
+4 -9
View File
@@ -4,22 +4,17 @@
</script> </script>
<Window> <Window>
<div class="flex flex-col items-center gap-4 p-6 text-center"> <div>
<h1 class="text-xl font-bold text-gray-900 dark:text-gray-100"> <h1>
Admin Error ({$page.status}) Admin Error ({$page.status})
</h1> </h1>
<p class="max-w-md text-sm text-gray-600 dark:text-gray-400"> <p>
{#if $page.error?.message} {#if $page.error?.message}
{$page.error.message} {$page.error.message}
{:else} {:else}
An unexpected error occurred while loading this admin page. An unexpected error occurred while loading this admin page.
{/if} {/if}
</p> </p>
<a <a href="/admin"> Back to Admin </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>
</div> </div>
</Window> </Window>
@@ -1,7 +1,6 @@
import type { LayoutServerLoad } from './$types'; import type { LayoutServerLoad } from './$types';
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
import { prisma } from '$lib/db'; import { prisma } from '$lib/db';
import { canAccessAdmin } from '$lib/svelte-admin/roles';
/** /**
* This Layout Server-Side page checks if the user is logged in * 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 } } } 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( console.log(
`Attempted to access /admin via ${user?.id} : ${user?.name} with a role of ${user?.role}` `Attempted to access /admin via ${user?.id} : ${user?.name} with a role of ${user?.role}`
); );
+3 -6
View File
@@ -1,8 +1,5 @@
import type { PageServerLoad } from './$types'; import { redirect } from '@sveltejs/kit';
import { getSerializableModelConfigs } from '$lib/svelte-admin/registry';
export const load: PageServerLoad = async ({ locals }) => { export const load = () => {
const viewerRoleId = (locals.user as { roleId?: number } | null)?.roleId ?? 4; redirect(307, '/admin/profiles');
const models = getSerializableModelConfigs(viewerRoleId, ['license-keys']);
return { models };
}; };
-21
View File
@@ -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 { error } from '@sveltejs/kit';
import { prisma } from '$lib/db'; import { prisma } from '$lib/db';
import { rotateApiKey } from '$lib/server/api-key'; import { rotateApiKey } from '$lib/server/api-key';
import { ROLES } from '$lib/svelte-admin/roles';
const SUPER = 1;
export const load: PageServerLoad = async ({ locals, parent }) => { export const load: PageServerLoad = async ({ locals, parent }) => {
const { adminUser } = await parent(); const { adminUser } = await parent();
if (adminUser.roleId > ROLES.SUPER) { if (adminUser.roleId > SUPER) {
throw error(403, 'Only Super admins can manage API keys'); 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 = { export const actions = {
rotate: async ({ locals }) => { rotate: async ({ locals }) => {
if ((await viewerRoleId(locals)) > ROLES.SUPER) { if ((await viewerRoleId(locals)) > SUPER) {
return { success: false, error: 'Not authorized' }; return { success: false, error: 'Not authorized' };
} }
@@ -62,7 +63,7 @@ export const actions = {
}; };
}, },
rotateEmergency: async ({ locals }) => { rotateEmergency: async ({ locals }) => {
if ((await viewerRoleId(locals)) > ROLES.SUPER) { if ((await viewerRoleId(locals)) > SUPER) {
return { success: false, error: 'Not authorized' }; return { success: false, error: 'Not authorized' };
} }
@@ -8,7 +8,6 @@
let mode = $state<'normal' | 'emergency' | null>(null); let mode = $state<'normal' | 'emergency' | null>(null);
let expiryDate = $state<string | null>(null); let expiryDate = $state<string | null>(null);
let toastMessage = $state(''); let toastMessage = $state('');
let toastType = $state<'success' | 'error'>('success');
function handleRotate(emergency: boolean) { function handleRotate(emergency: boolean) {
return ({ cancel }: any) => { return ({ cancel }: any) => {
@@ -29,7 +28,6 @@
} }
} else { } else {
toastMessage = 'Rotation failed'; toastMessage = 'Rotation failed';
toastType = 'error';
} }
}; };
}; };
@@ -38,7 +36,6 @@
function copyKey() { function copyKey() {
navigator.clipboard.writeText(newKey); navigator.clipboard.writeText(newKey);
toastMessage = 'Copied to clipboard'; toastMessage = 'Copied to clipboard';
toastType = 'success';
} }
function dismissKey() { function dismissKey() {
@@ -48,44 +45,45 @@
} }
</script> </script>
<div class="p-6"> <div class="mx-auto w-full">
<h1 class="mb-4 text-xl font-bold text-gray-900 dark:text-gray-100">API Key Management</h1> <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} {#if newKey}
<div <div class="mb-4 rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
class="mb-6 border border-amber-300 bg-amber-50 p-4 dark:border-amber-700 dark:bg-amber-900/30" <p class="text-sm font-medium text-slate-900">
>
<p class="mb-2 text-sm font-semibold text-amber-800 dark:text-amber-300">
{mode === 'emergency' ? 'Emergency Rotation Complete' : 'New API Key Generated'} {mode === 'emergency' ? 'Emergency Rotation Complete' : 'New API Key Generated'}
</p> </p>
<p class="mb-1 text-xs text-amber-700 dark:text-amber-400"> <p class="mt-1 text-sm text-slate-600">This key will not be shown again. Copy it now.</p>
This key will not be shown again. Copy it now. <div class="mt-3 rounded-md bg-slate-100 px-3 py-2">
</p> <code class="text-sm break-all text-slate-800">{newKey}</code>
<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>
</div> </div>
<div class="flex gap-2"> <div class="mt-3 flex gap-2">
<button <button
type="button" type="button"
onclick={copyKey} 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 Copy to Clipboard
</button> </button>
<button <button
type="button" type="button"
onclick={dismissKey} 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 Dismiss
</button> </button>
</div> </div>
{#if mode === 'normal' && expiryDate} {#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()} The previous key will remain valid until {new Date(expiryDate).toLocaleString()}
to give in-field devices time to update. to give in-field devices time to update.
</p> </p>
@@ -95,22 +93,18 @@
{#if toastMessage} {#if toastMessage}
<div <div
class="mb-4 border px-4 py-2 text-sm {toastType === 'success' class="mb-4 rounded-md border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700"
? 'border-green-300 bg-green-50 text-green-700'
: 'border-red-300 bg-red-50 text-red-700'}"
> >
{toastMessage} {toastMessage}
</div> </div>
{/if} {/if}
<div class="mb-6 rounded border border-gray-200 p-4 dark:border-gray-700"> <div class="mb-4 rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
<h2 class="mb-3 text-sm font-semibold text-gray-700 dark:text-gray-300">Current Key Status</h2> <h3 class="text-base font-semibold text-slate-900">Current Key Status</h3>
{#if data.activeKey} {#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> <div>
Prefix: <code class="font-mono font-medium text-gray-900 dark:text-gray-100" Prefix: <code class="rounded bg-slate-100 px-1.5 py-0.5">{data.activeKey.prefix}</code>
>{data.activeKey.prefix}</code
>
</div> </div>
<div>Created: {new Date(data.activeKey.createdAt).toLocaleString()}</div> <div>Created: {new Date(data.activeKey.createdAt).toLocaleString()}</div>
{#if data.activeKey.expiresAt} {#if data.activeKey.expiresAt}
@@ -118,20 +112,20 @@
{/if} {/if}
</div> </div>
{:else} {: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}
{#if data.totalRotated > 0} {#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. {data.totalRotated} previous key(s) have been rotated.
</p> </p>
{/if} {/if}
</div> </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)}> <form method="POST" action="?/rotate" use:enhance={handleRotate(false)}>
<button <button
type="submit" 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 Generate New API Key
</button> </button>
@@ -139,7 +133,7 @@
<form method="POST" action="?/rotateEmergency" use:enhance={handleRotate(true)}> <form method="POST" action="?/rotateEmergency" use:enhance={handleRotate(true)}>
<button <button
type="submit" 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 Emergency Rotation
</button> </button>
@@ -1,27 +1,19 @@
import type { PageServerLoad } from './$types'; 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 { prisma } from '$lib/db';
import { sendMail } from '$lib/server/resend'; import { sendMail } from '$lib/server/resend';
import { error } from '@sveltejs/kit'; 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 }) => { export const load: PageServerLoad = async () => {
const records = await loadRecords(locals, config); const records = await prisma.licenseKey.findMany({
include: { user: true },
const subscriptionLevels = await prisma.subscriptionLevel.findMany({ orderBy: { key: 'asc' }
select: { id: true, name: true },
orderBy: { name: 'asc' }
}); });
return { records: records as unknown as Record<string, unknown>[] };
return {
...records,
subscriptionLevels: subscriptionLevels.map((s: { id: number; name: string }) => ({
label: s.name,
value: s.name
}))
};
}; };
export const actions = { export const actions = {
@@ -29,41 +21,35 @@ export const actions = {
if (!locals.user) throw error(401, 'Unauthorized'); if (!locals.user) throw error(401, 'Unauthorized');
const formData = await request.formData(); const formData = await request.formData();
const email = formData.get('email')?.toString(); const email = formData.get('email')?.toString()?.trim().toLowerCase();
const level = formData.get('level')?.toString();
if (!email) { if (!email) {
return { success: false, error: 'Email is required' }; return { success: false, action: 'createKey', error: 'Email is required' };
}
if (!level) {
return { success: false, error: 'Subscription level is required' };
} }
const user = await prisma.user.findUnique({ where: { email } }); 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 = generateLicenseKey();
const key = `${hex.slice(0, 4)}-${hex.slice(4, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}`;
await prisma.licenseKey.create({ await prisma.licenseKey.create({
data: { data: {
key, key,
active: true, active: true,
level, userId: user?.id,
userId: user.id email: user ? undefined : email
} }
}); });
try { 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({ await sendMail({
to: email, to: email,
subject: 'Your Rhemawave License Key', subject: 'Your Radiant Wave License Key',
templateId: 'license-key-delivery', templateId: 'license-key-delivery',
data: { data: {
firstName: profile?.firstName ?? user.name ?? 'Valued Customer', firstName: profile?.firstName ?? user?.name ?? 'Valued Customer',
license_key: key license_key: key
} }
}); });
@@ -71,32 +57,29 @@ export const actions = {
console.error('Failed to send license key email:', e); console.error('Failed to send license key email:', e);
} }
return { success: true, error: undefined }; return { success: true, action: 'createKey' };
}, },
replace: async ({ request, locals }) => { replace: async ({ request, locals }) => {
if (!locals.user) throw error(401, 'Unauthorized'); if (!locals.user) throw error(401, 'Unauthorized');
const formData = await request.formData(); const formData = await request.formData();
const keyId = formData.get('id')?.toString(); 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 } }); 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 } }); 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({ await prisma.licenseKey.create({
data: { data: {
key, key: generateLicenseKey(),
active: true, 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"> <script lang="ts">
import type { PageProps } from './$types';
import { enhance } from '$app/forms'; 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 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 createOpen = $state(false);
let replaceOpen = $state(false); let replaceOpen = $state(false);
let replaceRecord = $state<Record<string, unknown> | null>(null); 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() { function openCreate() {
lastCreateForm = null;
createError = '';
createSuccess = '';
createOpen = true; createOpen = true;
} }
function openReplace(record: Record<string, unknown>) { function openReplace(record: Record<string, unknown>) {
replaceRecord = record; replaceRecord = record;
lastReplaceForm = null;
replaceError = '';
replaceSuccess = '';
replaceOpen = true; replaceOpen = true;
} }
@@ -34,98 +96,111 @@
replaceOpen = false; replaceOpen = false;
replaceRecord = null; 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> </script>
<div class="p-6"> <div>
<div class="mb-4 flex items-center justify-between"> <DataTable
<h1 class="text-xl font-bold text-gray-900 dark:text-gray-100">{config.displayName}</h1> title="License Keys"
<button {columns}
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
records={data.records} records={data.records}
listFields={config.listFields} newLabel="New License Key"
searchFields={config.searchFields} searchPlaceholder="Search license keys..."
fieldTypes={config.fieldTypes}
singular={config.displayNameSingular}
copyableFields={['key']} copyableFields={['key']}
onRowClick={openReplace} onRowClick={openReplace}
onNew={openCreate}
/> />
</div> </div>
<AdminModal title="Create License Key" open={createOpen} onClose={closeCreate}> <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 &amp; Email Key
</button>
</div>
</form>
</AdminModal> </AdminModal>
<AdminModal title="Replace License Key" open={replaceOpen} onClose={closeReplace}> <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} {#if replaceRecord}
<div class="flex flex-col gap-4"> <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>? Generate a new key to replace <strong>{String(replaceRecord.key)}</strong>?
</p> </p>
<div <p class="text-sm text-slate-500">
class="rounded border border-gray-200 bg-gray-50 p-3 text-sm dark:border-gray-700 dark:bg-gray-800" The existing key will be deleted and a new one will be created.
>
<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> </p>
<form
method="POST" <form method="POST" action="?/replace" use:enhance>
action="?/replace"
use:enhance={replaceEnhance}
class="flex justify-end gap-2 pt-2"
>
<input type="hidden" name="id" value={String(replaceRecord.id)} /> <input type="hidden" name="id" value={String(replaceRecord.id)} />
<button <div class="flex justify-end gap-3">
type="button" <button
onclick={closeReplace} type="button"
class="border border-gray-300 px-3 py-1.5 text-sm dark:border-gray-600 dark:text-gray-300" 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> Cancel
<button </button>
type="submit" <button
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" 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> Generate Replacement
</button>
</div>
</form> </form>
</div> </div>
{/if} {/if}
</AdminModal> </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/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', () => ({ vi.mock('$lib/db', () => ({
prisma: { prisma: {
user: { findUnique: vi.fn() }, user: { findUnique: vi.fn() },
profile: { findUnique: vi.fn() }, profile: { findFirst: vi.fn() },
licenseKey: { create: vi.fn() }, licenseKey: { findMany: vi.fn(), findUnique: vi.fn(), create: vi.fn(), delete: vi.fn() }
subscriptionLevel: { findMany: vi.fn() }
} }
})); }));
import { prisma } from '$lib/db'; 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 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 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', () => { describe('admin license-keys createKey action', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); 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 () => { it('rejects unauthenticated requests', async () => {
const locals = createLocals(); 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); const err = await actions.createKey({ request: req, locals } as any).catch((e) => e);
expect(isHttpError(err, 401)).toBe(true); expect(isHttpError(err, 401)).toBe(true);
@@ -47,27 +53,36 @@ describe('admin license-keys createKey action', () => {
it('returns error when email is missing', async () => { it('returns error when email is missing', async () => {
const locals = createLocals(mockUser() as any); const locals = createLocals(mockUser() as any);
const req = createFormRequest({ level: 'standard' }); const req = createFormRequest({});
const result = await actions.createKey({ request: req, locals } as any); 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 () => { it('creates a key with only an email when no user exists yet', 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 () => {
mockFindUnique.mockResolvedValue(null); mockFindUnique.mockResolvedValue(null);
mockProfileFindFirst.mockResolvedValue(null);
mockLicenseKeyCreate.mockResolvedValue({});
sendMail.mockResolvedValue({ id: 'email-1' });
const locals = createLocals(mockUser() as any); 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); 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 () => { 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' }; const profile = { firstName: 'Jane' };
mockFindUnique.mockResolvedValue(user); mockFindUnique.mockResolvedValue(user);
mockProfileFindUnique.mockResolvedValue(profile); mockProfileFindFirst.mockResolvedValue(profile);
mockLicenseKeyCreate.mockResolvedValue({}); mockLicenseKeyCreate.mockResolvedValue({});
sendMail.mockResolvedValue({ id: 'email-1' }); sendMail.mockResolvedValue({ id: 'email-1' });
const locals = createLocals(user as any); 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); 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(); expect(mockLicenseKeyCreate).toHaveBeenCalledOnce();
const createCall = mockLicenseKeyCreate.mock.calls[0][0]; 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.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.active).toBe(true);
expect(createCall.data.level).toBe('standard');
expect(createCall.data.userId).toBe('user-1'); expect(createCall.data.userId).toBe('user-1');
expect(sendMail).toHaveBeenCalledOnce(); expect(sendMail).toHaveBeenCalledOnce();
@@ -106,12 +120,12 @@ describe('admin license-keys createKey action', () => {
const user = mockUser(); const user = mockUser();
mockFindUnique.mockResolvedValue(user); mockFindUnique.mockResolvedValue(user);
mockProfileFindUnique.mockResolvedValue(null); mockProfileFindFirst.mockResolvedValue(null);
mockLicenseKeyCreate.mockResolvedValue({}); mockLicenseKeyCreate.mockResolvedValue({});
sendMail.mockResolvedValue({ id: 'email-1' }); sendMail.mockResolvedValue({ id: 'email-1' });
const locals = createLocals(user as any); 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); await actions.createKey({ request: req, locals } as any);
@@ -125,19 +139,76 @@ describe('admin license-keys createKey action', () => {
const user = mockUser(); const user = mockUser();
mockFindUnique.mockResolvedValue(user); mockFindUnique.mockResolvedValue(user);
mockProfileFindUnique.mockResolvedValue({ firstName: 'Jane' }); mockProfileFindFirst.mockResolvedValue({ firstName: 'Jane' });
mockLicenseKeyCreate.mockResolvedValue({}); mockLicenseKeyCreate.mockResolvedValue({});
sendMail.mockRejectedValue(new Error('Resend API down')); sendMail.mockRejectedValue(new Error('Resend API down'));
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const locals = createLocals(user as any); 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); 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(); expect(mockLicenseKeyCreate).toHaveBeenCalledOnce();
consoleSpy.mockRestore(); 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 type { PageServerLoad } from './$types';
import { getModelConfig } from '$lib/svelte-admin/registry'; import { prisma } from '$lib/db';
import { getDelegate } from '$lib/svelte-admin/prisma-delegate'; 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 }) => { export const load: PageServerLoad = async () => {
const delegate = getDelegate(config); const records = await prisma.proclamationCategory.findMany({
orderBy: [{ sortOrder: 'asc' }, { name: 'asc' }]
const records = await delegate.findMany({
where: { parentId: null },
include: config.include,
orderBy: { sortOrder: 'asc' }
}); });
return { records: records as unknown as Record<string, unknown>[] };
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 const actions = { export const actions = {
upsert: async ({ request, locals }) => { upsert: async ({ request }) => {
const formData = await request.formData(); const formData = await request.formData();
const form = await superValidate(formData, zod4(CategorySchema));
const name = (formData.get('name')?.toString() ?? '').trim(); if (!form.valid) {
const rawId = formData.get('id')?.toString(); const { error: formError, fieldErrors } = superFormErrorsToFieldErrors(
const childIds = formData.getAll('childId').map((v) => v.toString()); form.errors as Record<string, unknown>
const childNames = formData );
.getAll('childName') return { success: false, action: 'upsert', error: formError, fieldErrors };
.map((v) => v.toString().trim())
.filter(Boolean);
if (!name) {
return { success: false, error: 'Name is required', fieldErrors: { name: 'Required' } };
} }
const delegate = getDelegate(config); const rawId = formData.get('id')?.toString();
const name = form.data.name;
try { try {
let categoryId: number;
if (rawId) { if (rawId) {
const id = parseInt(rawId, 10); await prisma.proclamationCategory.update({
await delegate.update({ where: { id: Number(rawId) },
where: { id },
data: { name } data: { name }
}); });
categoryId = id;
} else { } else {
const maxSort = await delegate.findFirst({ const maxSort = await prisma.proclamationCategory.findFirst({
where: { parentId: null },
orderBy: { sortOrder: 'desc' }, orderBy: { sortOrder: 'desc' },
select: { sortOrder: true } select: { sortOrder: true }
}); });
const sortOrder = (maxSort?.sortOrder ?? -1) + 1; await prisma.proclamationCategory.create({
const created = await delegate.create({ data: { name, sortOrder: (maxSort?.sortOrder ?? -1) + 1 }
data: { name, sortOrder }
}); });
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) { } catch (e: unknown) {
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2002') { if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2002') {
return { return {
success: false, 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; throw e;
} }
return { success: true, action: 'upsert' };
}, },
delete: async ({ request, locals }) => { delete: async ({ request }) => {
const formData = await request.formData(); const formData = await request.formData();
const rawId = formData.get('id')?.toString(); const rawId = formData.get('id')?.toString();
if (!rawId) return { success: false, error: 'Missing id' }; if (!rawId) return { success: false, error: 'Missing id' };
const id = parseInt(rawId, 10); try {
const delegate = getDelegate(config); await prisma.proclamationCategory.delete({ where: { id: Number(rawId) } });
} catch (e: unknown) {
const viewerRoleId = (locals.user as { roleId?: number } | null)?.roleId ?? 4; if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2003') {
const where = config.accessFilter?.(viewerRoleId); return {
if (where) { success: false,
const match = await delegate.findFirst({ where: { id, ...where } }); error: 'Cannot delete: one or more proclamations use this category.'
if (!match) return { success: false, error: 'Access denied' }; };
}
throw e;
} }
await delegate.deleteMany({ where: { parentId: id } }); return { success: true, action: 'delete' };
await delegate.delete({ where: { id } });
return { success: true };
}, },
reorder: async ({ request }) => { reorder: async ({ request }) => {
const formData = await request.formData(); const formData = await request.formData();
@@ -148,16 +83,13 @@ export const actions = {
if (!raw) return { success: false, error: 'Missing ids' }; if (!raw) return { success: false, error: 'Missing ids' };
const ids = JSON.parse(raw) as number[]; const ids = JSON.parse(raw) as number[];
const delegate = getDelegate(config);
for (let i = 0; i < ids.length; i++) { for (let i = 0; i < ids.length; i++) {
await delegate.update({ await prisma.proclamationCategory.update({
where: { id: ids[i] }, where: { id: ids[i] },
data: { sortOrder: i } data: { sortOrder: i }
}); });
} }
return { success: true }; return { success: true, action: 'reorder' };
} }
}; };
@@ -1,36 +1,57 @@
<script lang="ts"> <script lang="ts">
import type { PageProps } from './$types'; import { enhance } from '$app/forms';
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 { invalidateAll } from '$app/navigation'; 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 { data, form }: PageProps = $props();
let canDelete = $derived((data.adminUser?.roleId ?? 4) <= 2);
const config = getModelConfig('proclamation-categories')!; const columns: DataTableColumn[] = [{ label: 'Name', value: (r) => r.name }];
const canDelete = $derived((data.adminUser?.roleId ?? 4) <= 2);
let modalOpen = $state(false); let modalOpen = $state(false);
let editingRecord = $state<Record<string, unknown> | null>(null); let editingRecord = $state<Record<string, unknown> | null>(null);
let toastMessage = $state('');
let toastType = $state<'success' | 'error'>('success'); let name = $state('');
let fieldErrors = $state<Record<string, string>>({});
let formError = $state(''); let formError = $state('');
let fieldErrors = $state<Record<string, string>>({});
let successMessage = $state('');
let lastForm = $state<ActionResult>(null);
$effect(() => { $effect(() => {
if (form && 'success' in (form as any)) { const rec = editingRecord ?? {};
const result = form as any; name = String(rec.name ?? '');
if (result.success) { if (!editingRecord) {
toastMessage = 'Saved successfully'; formError = '';
toastType = 'success'; fieldErrors = {};
closeModal(); successMessage = '';
} else { }
formError = result.error ?? ''; });
fieldErrors = result.fieldErrors ?? {};
} $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() { function openCreate() {
editingRecord = null; editingRecord = null;
formError = ''; lastForm = null;
fieldErrors = {};
modalOpen = true; modalOpen = true;
} }
function openEdit(record: Record<string, unknown>) { function openEdit(record: Record<string, unknown>) {
editingRecord = record; editingRecord = record;
formError = ''; lastForm = null;
fieldErrors = {};
modalOpen = true; modalOpen = true;
} }
function closeModal() { function closeModal() {
modalOpen = false; modalOpen = false;
editingRecord = null; editingRecord = null;
formError = '';
fieldErrors = {};
} }
</script> </script>
<div class="p-6"> <div>
<h1 class="mb-4 text-xl font-bold text-gray-900 dark:text-gray-100">{config.displayName}</h1> <DataTable
<AdminTable title="Proclamation Categories"
{columns}
records={data.records} records={data.records}
listFields={config.listFields} newLabel="New Category"
searchFields={config.searchFields} searchPlaceholder="Search categories..."
fieldTypes={config.fieldTypes}
singular={config.displayNameSingular}
draggable={true} draggable={true}
onReorder={handleReorder} onReorder={handleReorder}
onRowClick={openEdit} onRowClick={openEdit}
@@ -78,23 +94,62 @@
/> />
</div> </div>
<AdminModal {#if modalOpen}
title={editingRecord <AdminModal
? `Edit ${config.displayNameSingular}` title={editingRecord ? 'Edit Category' : 'Create Category'}
: `Create ${config.displayNameSingular}`} open={modalOpen}
open={modalOpen} onClose={closeModal}
onClose={closeModal} {canDelete}
{canDelete} recordId={editingRecord?.id as string | undefined}
recordId={editingRecord?.id as string | undefined} >
> {#if successMessage}
<CategoryForm <div
record={editingRecord ?? {}} class="mb-4 rounded-md border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700"
{fieldErrors} >
{formError} {successMessage}
submitLabel={editingRecord ? 'Save' : 'Create'} </div>
/> {/if}
</AdminModal> {#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} <form method="POST" action="?/upsert" use:enhance class="flex flex-col gap-4">
<Toast message={toastMessage} type={toastType} /> {#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} {/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 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 { prisma } from '$lib/db';
import { superValidate } from 'sveltekit-superforms/server'; import { superValidate } from 'sveltekit-superforms/server';
import { zod4 } from 'sveltekit-superforms/adapters'; 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')!; const ProclamationSchema = z.object({
name: z.string().min(1, 'Required'),
function viewerRoleId(locals: App.Locals): number { solfeggioFrequencyId: z.coerce.number().int(),
return (locals.user as { roleId?: number } | null)?.roleId ?? 4; categoryId: z.coerce.number().int(),
} groupId: z.coerce.number().int(),
content: z.string().optional().default('')
});
function isSuper(locals: App.Locals): boolean { 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 }) => { 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 // Strip content from non-Super users
if (!isSuper(locals)) { if (!isSuper(locals)) {
for (const record of base.records) { for (const record of records) {
delete record.content; delete record.content;
} }
} }
const categories = await prisma.proclamationCategory.findMany({ return { records, frequencies, categories, groups };
select: { id: true, name: true, parentId: true },
orderBy: { name: 'asc' }
});
return { ...base, categories };
}; };
export const actions = { export const actions = {
upsert: async ({ request, locals }) => { upsert: async ({ request, locals }) => {
if (!locals.user) { if (!locals.user) {
return { success: false, error: 'Unauthorized' }; return { success: false, action: 'upsert', error: 'Unauthorized' };
} }
if (!isSuper(locals)) { 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 formData = await request.formData();
const form = await superValidate(formData, zod4(Schemas.Proclamation)); const form = await superValidate(formData, zod4(ProclamationSchema));
if (!form.valid) { if (!form.valid) {
const { error, fieldErrors } = superFormErrorsToFieldErrors( const { error: formError, fieldErrors } = superFormErrorsToFieldErrors(
form.errors as Record<string, unknown> 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 rawId = formData.get('id')?.toString();
const { parentCategoryId, ...rest } = form.data; const data: Record<string, unknown> = { ...form.data };
const data: Record<string, unknown> = {
...rest,
categoryId: rest.categoryId ?? parentCategoryId
};
try { try {
if (rawId) { if (rawId) {
const existing = await prisma.proclamation.findUnique({ const existing = await prisma.proclamation.findUnique({ where: { id: rawId } });
where: { id: rawId }
});
if (!existing) { if (!existing) {
return { success: false, error: 'Record not found' }; return { success: false, action: 'upsert', error: 'Record not found' };
} }
await prisma.proclamation.update({ await prisma.proclamation.update({ where: { id: rawId }, data: data as any });
where: { id: rawId },
data: data as any
});
} else { } else {
await prisma.proclamation.create({ data: data as any }); 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') { if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2002') {
return { return {
success: false, success: false,
action: 'upsert',
error: 'A record with this name already exists.' error: 'A record with this name already exists.'
}; };
} }
throw e; throw e;
} }
return { success: true }; return { success: true, action: 'upsert' };
}, },
delete: async ({ request, locals }) => { delete: async ({ request, locals }) => {
if (!isSuper(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"> <script lang="ts">
import { enhance } from '$app/forms';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
import type { ActionResult } from '$lib/svelte-admin/types'; import { Editor } from '@tiptap/core';
import AdminTable from '$lib/components/admin/AdminTable.svelte'; 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 AdminModal from '$lib/components/admin/AdminModal.svelte';
import Toast from '$lib/components/admin/Toast.svelte';
import { getModelConfig } from '$lib/svelte-admin/registry'; type ActionResult = {
import ProclamationForm from './ProclamationForm.svelte'; success?: boolean;
action?: string;
error?: string;
fieldErrors?: Record<string, string>;
} | null;
let { data, form }: PageProps = $props(); let { data, form }: PageProps = $props();
const config = getModelConfig('proclamations')!;
const viewerRoleId = $derived(data.adminUser?.roleId ?? 4); const viewerRoleId = $derived(data.adminUser?.roleId ?? 4);
const isSuper = $derived(viewerRoleId <= 1); 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 modalOpen = $state(false);
let editingRecord = $state<Record<string, unknown> | null>(null); let editingRecord = $state<Record<string, unknown> | null>(null);
let toastMessage = $state('');
let toastType = $state<'success' | 'error'>('success'); let name = $state('');
let fieldErrors = $state<Record<string, string>>({}); let solfeggioFrequencyId = $state('');
let categoryId = $state('');
let groupId = $state('');
let content = $state('');
let formError = $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(() => { $effect(() => {
if (form && 'success' in (form as any)) { const record = editingRecord ?? {};
const result = form as any; name = String(record.name ?? '');
if (result.success) { solfeggioFrequencyId = String(record.solfeggioFrequencyId ?? '');
toastMessage = 'Saved successfully'; categoryId = String(record.categoryId ?? '');
toastType = 'success'; groupId = String(record.groupId ?? '');
closeModal(); content = typeof record.content === 'string' ? record.content : '';
} else { if (!editingRecord) {
formError = result.error ?? ''; formError = '';
fieldErrors = result.fieldErrors ?? {}; 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() { function openCreate() {
editingRecord = null; editingRecord = null;
formError = ''; lastForm = null;
fieldErrors = {};
modalOpen = true; modalOpen = true;
} }
function openEdit(record: Record<string, unknown>) { function openEdit(record: Record<string, unknown>) {
editingRecord = record; editingRecord = record;
formError = ''; lastForm = null;
fieldErrors = {};
modalOpen = true; modalOpen = true;
} }
function closeModal() { function closeModal() {
modalOpen = false; modalOpen = false;
editingRecord = null; editingRecord = null;
formError = '';
fieldErrors = {};
} }
</script> </script>
<div class="p-6"> <div>
<h1 class="mb-4 text-xl font-bold text-gray-900 dark:text-gray-100">{config.displayName}</h1> <DataTable
<AdminTable title="Proclamations"
{columns}
records={data.records} records={data.records}
listFields={config.listFields} newLabel="New Proclamation"
searchFields={config.searchFields} searchPlaceholder="Search proclamations..."
fieldTypes={config.fieldTypes}
singular={config.displayNameSingular}
onRowClick={isSuper ? openEdit : undefined} onRowClick={isSuper ? openEdit : undefined}
onNew={isSuper ? openCreate : undefined} onNew={isSuper ? openCreate : undefined}
/> />
</div> </div>
<AdminModal {#if modalOpen}
title={editingRecord <AdminModal
? `Edit ${config.displayNameSingular}` title={editingRecord ? 'Edit Proclamation' : 'Create Proclamation'}
: `Create ${config.displayNameSingular}`} open={modalOpen}
open={modalOpen} onClose={closeModal}
onClose={closeModal} canDelete={isSuper}
{canDelete} recordId={editingRecord?.id as string | undefined}
recordId={editingRecord?.id as string | undefined} >
> {#if successMessage}
<ProclamationForm <div
record={editingRecord ?? {}} class="mb-4 rounded-md border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700"
relationOptionsData={data.relationOptionsData} >
categories={data.categories} {successMessage}
{fieldErrors} </div>
{formError} {/if}
{isSuper} {#if formError}
submitLabel={editingRecord ? 'Update' : 'Create'} <div class="mb-4 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
/> {formError}
</AdminModal> </div>
{/if}
{#if toastMessage} <form method="POST" action="?/upsert" use:enhance class="flex flex-col gap-4">
<Toast message={toastMessage} type={toastType} /> {#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} {/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 { describe, it, expect, vi, beforeEach } from 'vitest';
import { isHttpError } from '@sveltejs/kit'; import { createLocals, mockUser, createFormRequest, mockProclamation } from '$lib/test-utils';
import { createLocals, mockUser, createFormRequest } from '$lib/test-utils';
vi.mock('$lib/db', () => ({ vi.mock('$lib/db', () => ({
prisma: { prisma: {
@@ -11,29 +10,44 @@ vi.mock('$lib/db', () => ({
update: vi.fn(), update: vi.fn(),
delete: vi.fn() delete: vi.fn()
}, },
solfeggioFrequency: {
findMany: vi.fn()
},
proclamationCategory: { proclamationCategory: {
findMany: vi.fn() 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 { prisma } from '$lib/db';
import { loadRecords, deleteRecord, superFormErrorsToFieldErrors } from '$lib/svelte-admin/crud';
import { load, actions } from './+page.server'; import { load, actions } from './+page.server';
const mockLoadRecords = loadRecords as ReturnType<typeof vi.fn>; const mockProclamationFindMany = prisma.proclamation.findMany as ReturnType<typeof vi.fn>;
const mockDeleteRecord = deleteRecord as ReturnType<typeof vi.fn>; const mockProclamationFindUnique = prisma.proclamation.findUnique as ReturnType<typeof vi.fn>;
const mockSuperFormErrorsToFieldErrors = superFormErrorsToFieldErrors 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 mockCatFindMany = prisma.proclamationCategory.findMany as ReturnType<typeof vi.fn>;
const mockFindUnique = prisma.proclamation.findUnique as ReturnType<typeof vi.fn>; const mockGroupFindMany = prisma.proclamationGroup.findMany 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 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', () => { describe('proclamations admin page server', () => {
beforeEach(() => { beforeEach(() => {
@@ -45,37 +59,42 @@ describe('proclamations admin page server', () => {
{ id: '1', name: 'A', content: '<p>secret</p>' }, { id: '1', name: 'A', content: '<p>secret</p>' },
{ id: '2', name: 'B', content: '<p>secret2</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 () => { 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 locals = createLocals(mockUser({ roleId: 4 }) as any);
const result = await load({ locals } 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({ expect(result).toEqual({
records: [ records: [
{ id: '1', name: 'A' }, { id: '1', name: 'A' },
{ id: '2', name: 'B' } { id: '2', name: 'B' }
], ],
relationOptionsData, frequencies,
categories categories,
groups
}); });
}); });
it('keeps content for Super users', async () => { it('keeps content for Super users', async () => {
mockLoadRecords.mockResolvedValue({ records: recordsWithContent, relationOptionsData });
mockCatFindMany.mockResolvedValue(categories);
const locals = createLocals(mockUser({ roleId: 1 }) as any); const locals = createLocals(mockUser({ roleId: 1 }) as any);
const result = await load({ locals } as any); const result = await load({ locals } as any);
expect(result).toEqual({ expect(result).toEqual({
records: recordsWithContent, records: recordsWithContent,
relationOptionsData, frequencies,
categories categories,
groups
}); });
}); });
}); });
@@ -86,7 +105,7 @@ describe('proclamations admin page server', () => {
const req = createFormRequest({ name: 'test' }); const req = createFormRequest({ name: 'test' });
const result = await actions.upsert({ request: req, locals } as any); 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 () => { 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); const result = await actions.upsert({ request: req, locals } as any);
expect(result).toEqual({ expect(result).toEqual({
success: false, success: false,
action: 'upsert',
error: 'Only Super admins can edit proclamations' 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', () => { describe('delete action', () => {
it('rejects non-Super users', async () => { it('rejects non-Super users', async () => {
const locals = createLocals(mockUser({ roleId: 4 }) as any); const locals = createLocals(mockUser({ roleId: 4 }) as any);
const req = new Request('http://localhost', { const req = createFormRequest({ id: '1' });
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ id: '1' })
});
const result = await actions.delete({ request: req, locals } as any); const result = await actions.delete({ request: req, locals } as any);
expect(result).toEqual({ expect(result).toEqual({
success: false, success: false,
action: 'delete',
error: 'Only Super admins can delete proclamations' error: 'Only Super admins can delete proclamations'
}); });
}); });
it('delegates to deleteRecord for Super users', async () => { it('returns an error when id is missing', async () => {
mockDeleteRecord.mockResolvedValue({ success: true });
const locals = createLocals(mockUser({ roleId: 1 }) as any); const locals = createLocals(mockUser({ roleId: 1 }) as any);
const req = new Request('http://localhost', { const req = createFormRequest({});
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ id: '1' })
});
const result = await actions.delete({ request: req, locals } as any); const result = await actions.delete({ request: req, locals } as any);
expect(mockDeleteRecord).toHaveBeenCalled(); expect(result).toEqual({ success: false, action: 'delete', error: 'Missing id' });
expect(result).toEqual({ success: true }); });
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 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 { prisma } from '$lib/db';
import { error } from '@sveltejs/kit';
import { superValidate } from 'sveltekit-superforms/server'; import { superValidate } from 'sveltekit-superforms/server';
import { zod4 } from 'sveltekit-superforms/adapters'; 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 }) => { const email = z.string().email('Invalid email address');
return loadRecords(locals, config); 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 = { export const actions = {
upsert: async ({ request, locals }) => { upsert: async ({ request }) => {
return upsertRecord(request, config, locals); const formData = await request.formData();
}, const form = await superValidate(formData, zod4(ProfileSchema));
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));
if (!form.valid) { if (!form.valid) {
const { error: addrError, fieldErrors } = superFormErrorsToFieldErrors( const { error: formError, fieldErrors } = superFormErrorsToFieldErrors(
form.errors as Record<string, unknown> 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 } = const { profileId, isDefaultMailing, isDefaultBilling, isDefaultShipping, ...addressData } =
form.data; form.data;
if (!profileId) { 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) { if (isDefaultMailing) {
await prisma.address.updateMany({ await prisma.address.updateMany({
where: { profileId, isDefaultMailing: true }, where: { profileId, isDefaultMailing: true },
@@ -72,32 +169,6 @@ export const actions = {
}; };
const created = await prisma.address.create({ data: payload }); const created = await prisma.address.create({ data: payload });
return { success: true, address: created }; return { success: true, action: 'upsertAddress', 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 };
} }
}; };
@@ -1,487 +1,59 @@
<script lang="ts"> <script lang="ts">
import type { PageProps } from './$types'; import type { PageProps } from './$types';
import type { ActionResult } from '$lib/svelte-admin/types'; import DataTable, { type DataTableColumn } from '$lib/components/DataTable.svelte';
import AdminTable from '$lib/components/admin/AdminTable.svelte'; import CreateProfileModal from './CreateProfileModal.svelte';
import AdminModal from '$lib/components/admin/AdminModal.svelte'; import EditProfileModal from './EditProfileModal.svelte';
import Toast from '$lib/components/admin/Toast.svelte';
import { enhance } from '$app/forms';
import { getModelConfig } from '$lib/svelte-admin/registry';
let { data, form }: PageProps = $props(); let { data, form }: PageProps = $props();
const config = getModelConfig('profiles')!;
let canDelete = $derived((data.adminUser?.roleId ?? 4) <= 2); let canDelete = $derived((data.adminUser?.roleId ?? 4) <= 2);
let modalOpen = $state(false); const columns: DataTableColumn[] = [
let editingRecord = $state<Record<string, unknown> | null>(null); { label: 'First Name', value: (r) => r.firstName },
let toastMessage = $state(''); { label: 'Last Name', value: (r) => r.lastName },
let toastType = $state<'success' | 'error'>('success'); {
let fieldErrors = $state<Record<string, string>>({}); label: 'Email',
let formError = $state(''); value: (r) => (r.user as Record<string, unknown> | undefined)?.email ?? r.email ?? ''
let addressFormOpen = $state(false); },
let userFormOpen = $state(false); { label: 'Phone', value: (r) => r.phoneNumber },
let addressError = $state(''); { label: 'User', value: (r) => (r.user as Record<string, unknown> | undefined)?.name ?? '' }
let userError = $state(''); ];
let addressFields = $state<Record<string, string>>({ let createOpen = $state(false);
street: '', let editRecord = $state<Record<string, unknown> | null>(null);
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 ?? {};
}
}
});
function openCreate() { function openCreate() {
editingRecord = null; createOpen = true;
formError = '';
fieldErrors = {};
addressFormOpen = false;
userFormOpen = false;
modalOpen = true;
} }
function openEdit(record: Record<string, unknown>) { function openEdit(record: Record<string, unknown>) {
editingRecord = record; editRecord = 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;
} }
function closeModal() { function closeCreate() {
modalOpen = false; createOpen = false;
editingRecord = null;
formError = '';
fieldErrors = {};
addressFormOpen = false;
userFormOpen = false;
addressError = '';
userError = '';
} }
function toggleAddressForm() { function closeEdit() {
addressFormOpen = !addressFormOpen; editRecord = null;
addressError = '';
}
function toggleUserForm() {
userFormOpen = !userFormOpen;
userError = '';
} }
</script> </script>
<div class="p-6"> <div>
<h1 class="mb-4 text-xl font-bold text-gray-900 dark:text-gray-100">{config.displayName}</h1> <DataTable
<AdminTable title="User Profiles"
{columns}
records={data.records} records={data.records}
listFields={config.listFields} newLabel="New Profile"
searchFields={config.searchFields} searchPlaceholder="Search profiles..."
fieldTypes={config.fieldTypes}
singular={config.displayNameSingular}
onRowClick={openEdit} onRowClick={openEdit}
onNew={openCreate} onNew={openCreate}
/> />
</div> </div>
<AdminModal <CreateProfileModal open={createOpen} onClose={closeCreate} {form} />
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>
{#if editingRecord} {#if editRecord}
{@const user = editingRecord.user as Record<string, unknown> | undefined} {#key editRecord.id}
<div class="mt-6 border-t border-gray-200 pt-4 dark:border-gray-700"> <EditProfileModal record={editRecord} open={true} onClose={closeEdit} {canDelete} {form} />
<div class="mb-2 flex items-center justify-between"> {/key}
<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} {/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 type { PageServerLoad } from './$types';
import { getModelConfig } from '$lib/svelte-admin/registry'; import { prisma } from '$lib/db';
import { loadRecords, upsertRecord, deleteRecord } from '$lib/svelte-admin/crud'; 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 }) => { export const load: PageServerLoad = async () => {
return loadRecords(locals, config); const records = await prisma.solfeggioFrequency.findMany({ orderBy: { id: 'asc' } });
return { records: records as unknown as Record<string, unknown>[] };
}; };
export const actions = { export const actions = {
upsert: async ({ request, locals }) => { upsert: async ({ request }) => {
return upsertRecord(request, config, locals); 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 }) => { delete: async ({ request }) => {
return deleteRecord(request, config, locals); 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"> <script lang="ts">
import { enhance } from '$app/forms';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
import type { ActionResult } from '$lib/svelte-admin/types'; import DataTable, { type DataTableColumn } from '$lib/components/DataTable.svelte';
import AdminTable from '$lib/components/admin/AdminTable.svelte';
import AdminModal from '$lib/components/admin/AdminModal.svelte'; import AdminModal from '$lib/components/admin/AdminModal.svelte';
import Toast from '$lib/components/admin/Toast.svelte';
import { getModelConfig } from '$lib/svelte-admin/registry'; type ActionResult = {
import FreqForm from './FreqForm.svelte'; success?: boolean;
action?: string;
error?: string;
fieldErrors?: Record<string, string>;
} | null;
let { data, form }: PageProps = $props(); let { data, form }: PageProps = $props();
let canDelete = $derived((data.adminUser?.roleId ?? 4) <= 2);
const config = getModelConfig('solfeggio-frequencies')!; const columns: DataTableColumn[] = [
const canDelete = $derived((data.adminUser?.roleId ?? 4) <= 2); { label: 'Name', value: (r) => r.name },
{ label: 'Value', value: (r) => r.value }
];
let modalOpen = $state(false); let modalOpen = $state(false);
let editingRecord = $state<Record<string, unknown> | null>(null); let editingRecord = $state<Record<string, unknown> | null>(null);
let toastMessage = $state('');
let toastType = $state<'success' | 'error'>('success'); let name = $state('');
let fieldErrors = $state<Record<string, string>>({}); let value = $state('');
let formError = $state(''); let formError = $state('');
let fieldErrors = $state<Record<string, string>>({});
let successMessage = $state('');
let lastForm = $state<ActionResult>(null);
$effect(() => { $effect(() => {
if (form && 'success' in (form as any)) { const rec = editingRecord ?? {};
const result = form as any; name = String(rec.name ?? '');
if (result.success) { value = String(rec.value ?? '');
toastMessage = 'Saved successfully'; if (!editingRecord) {
toastType = 'success'; formError = '';
closeModal(); fieldErrors = {};
} else { successMessage = '';
formError = result.error ?? ''; }
fieldErrors = result.fieldErrors ?? {}; });
}
$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() { function openCreate() {
editingRecord = null; editingRecord = null;
formError = ''; lastForm = null;
fieldErrors = {};
modalOpen = true; modalOpen = true;
} }
function openEdit(record: Record<string, unknown>) { function openEdit(record: Record<string, unknown>) {
editingRecord = record; editingRecord = record;
formError = ''; lastForm = null;
fieldErrors = {};
modalOpen = true; modalOpen = true;
} }
function closeModal() { function closeModal() {
modalOpen = false; modalOpen = false;
editingRecord = null; editingRecord = null;
formError = '';
fieldErrors = {};
} }
</script> </script>
<div class="p-6"> <div>
<h1 class="mb-4 text-xl font-bold text-gray-900 dark:text-gray-100">{config.displayName}</h1> <DataTable
<AdminTable title="Solfeggio Frequencies"
{columns}
records={data.records} records={data.records}
listFields={config.listFields} newLabel="New Frequency"
searchFields={config.searchFields} searchPlaceholder="Search frequencies..."
fieldTypes={config.fieldTypes}
singular={config.displayNameSingular}
onRowClick={openEdit} onRowClick={openEdit}
onNew={openCreate} onNew={openCreate}
/> />
</div> </div>
<AdminModal {#if modalOpen}
title={editingRecord <AdminModal
? `Edit ${config.displayNameSingular}` title={editingRecord ? 'Edit Frequency' : 'Create Frequency'}
: `Create ${config.displayNameSingular}`} open={modalOpen}
open={modalOpen} onClose={closeModal}
onClose={closeModal} {canDelete}
{canDelete} recordId={editingRecord?.id as string | undefined}
recordId={editingRecord?.id as string | undefined} >
> {#if successMessage}
<FreqForm <div
record={editingRecord ?? {}} class="mb-4 rounded-md border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700"
{fieldErrors} >
{formError} {successMessage}
submitLabel={editingRecord ? 'Update' : 'Create'} </div>
/> {/if}
</AdminModal> {#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} <form method="POST" action="?/upsert" use:enhance class="flex flex-col gap-4">
<Toast message={toastMessage} type={toastType} /> {#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} {/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 type { PageServerLoad } from './$types';
import { getModelConfig } from '$lib/svelte-admin/registry'; import { prisma } from '$lib/db';
import { loadRecords, upsertRecord, deleteRecord } from '$lib/svelte-admin/crud'; 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 }) => { export const load: PageServerLoad = async () => {
return loadRecords(locals, config); const records = await prisma.userRole.findMany({ orderBy: { id: 'asc' } });
return { records: records as unknown as Record<string, unknown>[] };
}; };
export const actions = { export const actions = {
upsert: async ({ request, locals }) => { upsert: async ({ request }) => {
return upsertRecord(request, config, locals); 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 }) => { delete: async ({ request }) => {
return deleteRecord(request, config, locals); 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"> <script lang="ts">
import { enhance } from '$app/forms';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
import type { ActionResult } from '$lib/svelte-admin/types'; import DataTable, { type DataTableColumn } from '$lib/components/DataTable.svelte';
import AdminTable from '$lib/components/admin/AdminTable.svelte';
import AdminModal from '$lib/components/admin/AdminModal.svelte'; import AdminModal from '$lib/components/admin/AdminModal.svelte';
import Toast from '$lib/components/admin/Toast.svelte';
import { getModelConfig } from '$lib/svelte-admin/registry'; type ActionResult = {
import UserRoleForm from './UserRoleForm.svelte'; success?: boolean;
action?: string;
error?: string;
fieldErrors?: Record<string, string>;
} | null;
let { data, form }: PageProps = $props(); let { data, form }: PageProps = $props();
let canDelete = $derived((data.adminUser?.roleId ?? 4) <= 2);
const config = getModelConfig('user-roles')!; const columns: DataTableColumn[] = [
const canDelete = $derived((data.adminUser?.roleId ?? 4) <= 2); { label: 'Id', value: (r) => r.id },
{ label: 'Name', value: (r) => r.name }
];
let modalOpen = $state(false); let modalOpen = $state(false);
let editingRecord = $state<Record<string, unknown> | null>(null); let editingRecord = $state<Record<string, unknown> | null>(null);
let toastMessage = $state('');
let toastType = $state<'success' | 'error'>('success'); let name = $state('');
let fieldErrors = $state<Record<string, string>>({});
let formError = $state(''); let formError = $state('');
let fieldErrors = $state<Record<string, string>>({});
let successMessage = $state('');
let lastForm = $state<ActionResult>(null);
$effect(() => { $effect(() => {
if (form && 'success' in (form as any)) { const rec = editingRecord ?? {};
const result = form as any; name = String(rec.name ?? '');
if (result.success) { if (!editingRecord) {
toastMessage = 'Saved successfully'; formError = '';
toastType = 'success'; fieldErrors = {};
closeModal(); successMessage = '';
} else { }
formError = result.error ?? ''; });
fieldErrors = result.fieldErrors ?? {};
} $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() { function openCreate() {
editingRecord = null; editingRecord = null;
formError = ''; lastForm = null;
fieldErrors = {};
modalOpen = true; modalOpen = true;
} }
function openEdit(record: Record<string, unknown>) { function openEdit(record: Record<string, unknown>) {
editingRecord = record; editingRecord = record;
formError = ''; lastForm = null;
fieldErrors = {};
modalOpen = true; modalOpen = true;
} }
function closeModal() { function closeModal() {
modalOpen = false; modalOpen = false;
editingRecord = null; editingRecord = null;
formError = '';
fieldErrors = {};
} }
</script> </script>
<div class="p-6"> <div>
<h1 class="mb-4 text-xl font-bold text-gray-900 dark:text-gray-100">{config.displayName}</h1> <DataTable
<AdminTable title="User Roles"
{columns}
records={data.records} records={data.records}
listFields={config.listFields} newLabel="New Role"
searchFields={config.searchFields} searchPlaceholder="Search roles..."
fieldTypes={config.fieldTypes}
singular={config.displayNameSingular}
onRowClick={openEdit} onRowClick={openEdit}
onNew={openCreate} onNew={openCreate}
/> />
</div> </div>
<AdminModal {#if modalOpen}
title={editingRecord <AdminModal
? `Edit ${config.displayNameSingular}` title={editingRecord ? 'Edit User Role' : 'Create User Role'}
: `Create ${config.displayNameSingular}`} open={modalOpen}
open={modalOpen} onClose={closeModal}
onClose={closeModal} {canDelete}
{canDelete} recordId={editingRecord?.id as string | undefined}
recordId={editingRecord?.id as string | undefined} >
> {#if successMessage}
<UserRoleForm <div
record={editingRecord ?? {}} class="mb-4 rounded-md border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700"
{fieldErrors} >
{formError} {successMessage}
submitLabel={editingRecord ? 'Update' : 'Create'} </div>
/> {/if}
</AdminModal> {#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} <form method="POST" action="?/upsert" use:enhance class="flex flex-col gap-4">
<Toast message={toastMessage} type={toastType} /> {#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} {/if}

Some files were not shown because too many files have changed in this diff Show More