Compare commits

..
32 Commits
Author SHA1 Message Date
NeilClack 960673bbc6 Add a site-wide dark mode
Build and Push to Digital Ocean Container Registry / build-and-push (push) Failing after 4s
Build and Push to Digital Ocean Container Registry / migrate (push) Skipped
2026-09-11 12:49:46 -05:00
NeilClack a909db39b1 Delete "API Key" page 2026-09-11 12:44:44 -05:00
NeilClack 020289966d Remove "User Roles" admin page 2026-09-11 12:44:25 -05:00
NeilClack fa614b8c37 Fix staff(+) unable to edit or delete addresses 2026-09-11 12:43:41 -05:00
NeilClack cf8fc7b719 Fix users unable to edit or delete their own addresses 2026-09-11 12:42:35 -05:00
NeilClack 848e646880 Remove the extra page separator in the Login page 2026-09-11 12:41:49 -05:00
NeilClack 98d9b095d9 Document v2 API with OpenAPI schema 2026-09-10 16:19:53 -05:00
NeilClack 2bbee98594 Add dedicated v2 proclamation category/group endpoints and catalog specs 2026-09-10 16:19:53 -05:00
NeilClack 4d929df604 Remove stale admin overhaul and API update docs 2026-09-10 16:19:52 -05:00
NeilClack 10aad07e6d Add admin Audit Log page 2026-09-10 15:10:16 -05:00
NeilClack d16965dab6 Add AuditLog table and Prisma audit extension 2026-09-10 15:10:11 -05:00
NeilClack 7596937f30 Enforce roleId checks on all admin page actions 2026-09-10 14:59:32 -05:00
NeilClack 0cb4b2148a Expose roleId on the Better Auth session user 2026-09-10 14:59:28 -05:00
NeilClack 9de0e2e25a Fix Super admins being blocked from editing proclamations 2026-09-10 14:50:08 -05:00
NeilClack 9cd5397395 Add Resend email template HTML files 2026-09-10 14:35:54 -05:00
NeilClack f6b8cad15e Add subject to password reset email 2026-09-10 14:14:45 -05:00
NeilClack 29d53af8dd Remove unauthenticated /send email endpoint 2026-09-10 14:14:44 -05:00
NeilClack 809ced6d8a Automated signups use the registration-email set-password link 2026-09-10 14:08:56 -05:00
NeilClack 84c905c9ff Add shared registration invite helper and use it for admin-created users 2026-09-10 14:08:51 -05:00
NeilClack ae8d661364 Fix reset-password page to look up the reset token correctly 2026-09-10 13:42:36 -05:00
NeilClack 5ec3018f8b Email a set-password link when an admin creates a user 2026-09-10 13:42:33 -05:00
NeilClack 8a3c1587b4 Fix admin modals getting stuck after a successful submit 2026-09-10 13:33:33 -05:00
NeilClack e60d61e90a Fix admin modals replaying old action results after create or edit 2026-09-10 13:26:54 -05:00
NeilClack f1d80279e9 Record last validation time in v2 license endpoint 2026-09-10 13:18:22 -05:00
NeilClack 4979d955c2 Show active check and last verified date on dashboard license table 2026-09-10 13:18:18 -05:00
NeilClack 0175cab0b4 Add lastValidatedAt to license keys 2026-09-10 13:18:15 -05:00
NeilClack 1aecc51bbd Replace account and license-keys pages with profile page and dashboard 2026-09-10 13:11:41 -05:00
NeilClack f50eb34769 Add createdAt to license keys 2026-09-10 13:11:34 -05:00
NeilClack 4a1777de87 Keep legacy subscription tables as unmanaged instead of dropping them 2026-09-10 11:48:49 -05:00
NeilClack d89fc6837c Add default roleId 4 to new users 2026-09-10 11:48:20 -05:00
NeilClack 5e8851e070 Fix lint debt from admin overhaul; make check/lint/test/build green
- Scope @typescript-eslint/no-explicit-any off for *.spec.ts test doubles
- Type enhance() submit callbacks with SubmitFunction in api-keys page
- Drop as-any casts in proclamations upsert by using typed form data
- Fix useless regex escapes in profiles phone validation
- Remove unused locals param from api-keys load
- Use resolve() for admin nav/error links; store literal hrefs in adminModels
- Update README admin section and ADMIN_OVERHAUL lint caveat
2026-09-10 09:43:16 -05:00
NeilClack 91d162c746 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.
2026-09-10 09:38:01 -05:00
176 changed files with 12536 additions and 10621 deletions
+4 -1
View File
@@ -5,5 +5,8 @@ GOOGLE_CLIENT_ID=6
GOOGLE_CLIENT_SECRET=
RESEND_API_KEY=
EMAIL_FROM=
RESEND_TEMPLATE_ID=
# Optional. Resend template for the admin "set your password" invite email.
# Defaults to `registration-email`. Expected variables:
# `target_email_address`, `registration_link`.
RESEND_REGISTRATION_TEMPLATE_ID=
API_V2_ONBOARDING_SECRET=
+3
View File
@@ -7,3 +7,6 @@ bun.lockb
# Miscellaneous
/static/
# Generated code
/src/lib/generated/
+30 -33
View File
@@ -9,11 +9,11 @@ AGENTS.md
at your project root:
````text
# Rhemawave Portal AI Guide
# Radiant Wave Portal AI Guide
## 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.
@@ -97,9 +97,9 @@ Use narrower commands when appropriate.
If validation fails:
* Diagnose the failure.
* Fix the issue.
* Run validation again.
- Diagnose the failure.
- Fix the issue.
- Run validation again.
Never finish with known failures.
@@ -109,19 +109,19 @@ Never finish with known failures.
Application:
portal.Rhemawavetech.com
portal.radiantwavetech.com
Stack:
* SvelteKit 2
* Svelte 5
* TypeScript
* Tailwind CSS v4
* Prisma 7
* PostgreSQL
* Better Auth
* Vitest
* Playwright
- SvelteKit 2
- Svelte 5
- TypeScript
- Tailwind CSS v4
- Prisma 7
- PostgreSQL
- Better Auth
- Vitest
- Playwright
Package manager:
@@ -266,9 +266,9 @@ AdminPage.svelte
Registry metadata exists for:
* columns
* searches
* relations
- columns
- searches
- relations
Forms are manually implemented per route.
@@ -309,8 +309,8 @@ Only load that file when modifying Svelte components or Svelte logic.
Testing uses:
* Vitest
* Playwright browser testing
- Vitest
- Playwright browser testing
For testing patterns, read:
@@ -338,10 +338,10 @@ Only load that file when working on API functionality.
Prettier rules:
* tabs
* single quotes
* no trailing commas
* 80 character print width
- tabs
- single quotes
- no trailing commas
- 80 character print width
Follow existing formatting.
@@ -391,17 +391,14 @@ Confirm:
2. Relevant tests/checks were run.
3. Failures were fixed.
4. The final response explains:
* what changed
* files modified
* verification performed
- what changed
- files modified
- verification performed
Do not stop at:
* "I found the issue"
* "Here is a plan"
* "The next step would be"
- "I found the issue"
- "Here is a plan"
- "The next step would be"
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.
@@ -332,7 +332,7 @@ The `$effect` resets `dirty = false` on mount (when the record prop changes).
| Template ID | Variables | Subject (API-level) |
| ------------------------------ | --------------------------------------------------------------- | ------------------------------ |
| `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" |
Variables must be declared in the Resend template editor's Variables section. Undeclared variables render as literal `{{{varName}}}` text.
+14 -15
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,
and an admin-facing platform management application. An all-in-one management platform for Radiant Wave and Radiant Wave Technologies.
@@ -36,18 +36,17 @@ podman push registry.digitalocean.com/radiantwave-portal-container-registry/port
## Admin
Admin CRUD pages at `/admin`. 9 models have per-page routes with modals for create/edit:
Admin pages live under `/admin` (requires `roleId <= 3`); `/admin` redirects to
`/admin/profiles`. Each route owns its own form and modal logic, built on the shared
`DataTable.svelte` and `AdminModal.svelte` components. The generic `$lib/svelte-admin`
CRUD layer has been removed; see `docs/ADMIN_OVERHAUL.md` for the migration notes.
| Included | Excluded |
| ------------------------ | --------------------------------- |
| users | Session (Better Auth) |
| profiles | Account (Better Auth) |
| addresses | Verification (Better Auth) |
| user-roles | UserProclamations (composite key) |
| proclamations | |
| solfeggio-frequencies | |
| proclamation-categories | |
| license-keys (list-only) | |
| subscription-levels | |
See `src/lib/svelte-admin/README.md` for architecture and configuration docs.
| Route | Notes |
| -------------------------------- | ------------------------ |
| `/admin/users` | Name, Email, Role |
| `/admin/profiles` | Reference implementation |
| `/admin/proclamations` | Super-only content edits |
| `/admin/solfeggio-frequencies` | |
| `/admin/proclamation-categories` | Draggable reorder |
| `/admin/proclamation-groups` | Draggable reorder |
| `/admin/license-keys` | List-only + rotate |
+2 -4
View File
@@ -89,8 +89,8 @@ POST:
Payload:
{
"license_key": "...",
"email_address": "..."
"license_key": "...",
"email_address": "..."
}
Requires API key.
@@ -153,7 +153,6 @@ Variables:
- verifyUrl
- loginUrl
license-key-delivery
Variables:
@@ -161,7 +160,6 @@ Variables:
- firstName
- license_key
email-verification-email
Variables:
+13 -12
View File
@@ -36,13 +36,13 @@ Always destructure $props() with defaults.
Preferred pattern:
let {
record = {} as Record<string, unknown>,
fieldErrors = {} as Record<string, string>,
isSuper = false
record = {} as Record<string, unknown>,
fieldErrors = {} as Record<string, string>,
isSuper = false
}: {
record?: Record<string, unknown>;
fieldErrors?: Record<string, string>;
isSuper?: boolean;
record?: Record<string, unknown>;
fieldErrors?: Record<string, string>;
isSuper?: boolean;
} = $props();
Do not assume props exist.
@@ -66,7 +66,7 @@ Correct:
let name = $state('');
$effect(() => {
name = String(record.name ?? '');
name = String(record.name ?? '');
});
Use $effect whenever component state needs to follow changing props.
@@ -88,11 +88,11 @@ Example:
let dirty = $state(false);
$effect(() => {
dirty = false;
dirty = false;
});
$effect(() => {
name = String(record.name ?? '');
name = String(record.name ?? '');
});
This ensures a new record starts clean.
@@ -116,9 +116,10 @@ This can fail to update correctly.
Use:
<select onchange={(e) => frequency = e.currentTarget.value}>
<option value="1" selected={frequency === '1'}>
One
</option>
<option value="1" selected={frequency === '1'}>
One
</option>
</select>
Use selected on options.
+29 -29
View File
@@ -30,13 +30,13 @@ Tests are colocated with source files.
Client tests:
*.svelte.test.ts
*.svelte.spec.ts
_.svelte.test.ts
_.svelte.spec.ts
Server tests:
*.test.ts
*.spec.ts
_.test.ts
_.spec.ts
---
@@ -79,7 +79,7 @@ Node.
Always reset mocks:
beforeEach(() => {
vi.clearAllMocks();
vi.clearAllMocks();
});
---
@@ -109,15 +109,15 @@ Mock only the methods used.
Example:
vi.mock('$lib/db', () => ({
prisma: {
proclamation: {
findMany: vi.fn(),
findUnique: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn()
}
}
prisma: {
proclamation: {
findMany: vi.fn(),
findUnique: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn()
}
}
}));
Do not create massive unused mocks.
@@ -143,7 +143,7 @@ createLocals(mockUser() as any)
Specific roles:
createLocals(
mockUser({ roleId: 4 }) as any
mockUser({ roleId: 4 }) as any
)
---
@@ -172,15 +172,15 @@ Always use exact regex names.
Preferred:
page.getByRole(
'textbox',
{ name: /^Name$/ }
'textbox',
{ name: /^Name$/ }
)
Avoid:
page.getByRole(
'textbox',
{ name: 'Name' }
'textbox',
{ name: 'Name' }
)
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:
const element =
await page
.getByRole(
'combobox',
{ name: /^Frequency$/ }
)
.element();
await page
.getByRole(
'combobox',
{ name: /^Frequency$/ }
)
.element();
expect(
(element as HTMLSelectElement).value
(element as HTMLSelectElement).value
).toBe('2');
---
@@ -213,9 +213,9 @@ Use:
vi.mock('$lib/components/example.svelte', () => ({
default: ($anchor: Element) => {
const div = document.createElement('div');
$anchor.replaceWith(div);
}
const div = document.createElement('div');
$anchor.replaceWith(div);
}
}));
Imports using mocked modules must come after vi.mock().
+467
View File
@@ -0,0 +1,467 @@
openapi: 3.0.3
info:
title: Radiant Wave Portal API (v2)
version: 2.0.0
description: |
Read API used by the Radiant Wave kiosk plus the automation onboarding
webhook.
## Authentication
The v2 API is **not** API-key gated. The former bearer-token middleware in
`src/hooks.server.ts` is present but disabled, so v2 routes are served
without a global API key.
Two endpoints carry their own credential semantics:
- `GET /api/v2/proclamations` optionally accepts the paired
`x-license-key` / `x-email` headers to scope results to an entitled user.
Supplying neither returns the full catalogue; supplying only one, or a
pair that does not match, is rejected.
- `POST /api/v2/automations/create/account` is protected by an HMAC-SHA256
request signature (see the operation description).
servers:
- url: https://portal.radiantwavetech.com
description: Production
- url: http://localhost:5173
description: Local development
tags:
- name: Catalogue
description: Reference data for categories, groups, frequencies and palettes.
- name: Proclamations
description: Proclamation content, optionally scoped by license entitlement.
- name: License
description: License key validation.
- name: Automations
description: Signed machine-to-machine webhooks.
paths:
/api/v2/categories:
get:
tags: [Catalogue]
summary: List proclamation categories
operationId: listCategories
responses:
'200':
description: All proclamation categories.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Category'
/api/v2/proclamation-categories:
get:
tags: [Catalogue]
summary: List proclamation categories (dedicated alias)
description: Alias of `/api/v2/categories`; returns the identical payload.
operationId: listProclamationCategories
responses:
'200':
description: All proclamation categories.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Category'
/api/v2/groups:
get:
tags: [Catalogue]
summary: List proclamation groups
description: Groups are ordered by `sortOrder` ascending.
operationId: listGroups
responses:
'200':
description: All proclamation groups.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Group'
/api/v2/proclamation-groups:
get:
tags: [Catalogue]
summary: List proclamation groups (dedicated alias)
description: Alias of `/api/v2/groups`; returns the identical payload.
operationId: listProclamationGroups
responses:
'200':
description: All proclamation groups.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Group'
/api/v2/frequencies:
get:
tags: [Catalogue]
summary: List canonical solfeggio frequencies
description: |
Returns the nine canonical frequencies (ids 1-9) the kiosk expects.
These are static values, not the portal database autoincrement ids.
operationId: listFrequencies
responses:
'200':
description: The nine canonical frequencies.
content:
application/json:
schema:
type: array
minItems: 9
maxItems: 9
items:
$ref: '#/components/schemas/Frequency'
/api/v2/color-palettes:
get:
tags: [Catalogue]
summary: List color palettes per canonical frequency
description: |
Returns one 7-color palette for each canonical frequency id (1-9).
Palette rows stored in the database take precedence; ids without a row
fall back to the built-in canonical palette.
operationId: listColorPalettes
responses:
'200':
description: Nine palettes, one per canonical frequency id.
content:
application/json:
schema:
type: array
minItems: 9
maxItems: 9
items:
$ref: '#/components/schemas/ColorPalette'
/api/v2/proclamations:
get:
tags: [Proclamations]
summary: List proclamations
description: |
Without entitlement headers, returns the full proclamation catalogue.
With both `x-license-key` and `x-email`, returns only the proclamations
assigned to the matching user. Both headers must be present together and
the key must belong to the user.
Proclamations whose frequency has no canonical id (for example 936 Hz)
are omitted from the response.
operationId: listProclamations
parameters:
- in: header
name: x-license-key
schema:
type: string
required: false
description: License key. Must be supplied together with `x-email`.
- in: header
name: x-email
schema:
type: string
format: email
required: false
description: Account email. Must be supplied together with `x-license-key`.
responses:
'200':
description: Proclamation records with canonical frequency ids and embedded palettes.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Proclamation'
'400':
description: |
Missing entitlement or the supplied key/email pair is invalid.
Message is `not-authorized`, or
`this key doesn't appear to belong to you` when the key does not
belong to the specified user.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
/api/v2/license:
post:
tags: [License]
summary: Validate a license key
description: |
Validates that the license key exists, is active, and belongs to the
account matching `email_address`. A successful validation records
`lastValidatedAt` on the key.
operationId: validateLicense
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LicenseRequest'
responses:
'200':
description: The validated key and its owner email.
content:
application/json:
schema:
$ref: '#/components/schemas/LicenseResponse'
'400':
description: |
Validation failure or malformed body. Possible messages:
`content-type must be application/json`, `invalid JSON body`,
`request body is required`, `license_key is required`,
`email_address is required`, `license key not found`,
`license key is not active`, `user does not exist`,
`key does not belong to specified user`.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
'413':
description: The request body exceeded the 64 KiB limit.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
'429':
description: Rate limit exceeded (10 requests per minute per IP).
headers:
Retry-After:
schema:
type: integer
description: Seconds to wait before retrying.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
/api/v2/automations/create/account:
post:
tags: [Automations]
summary: Create an account and license key (signed webhook)
description: |
Machine-to-machine onboarding webhook. The request must be signed with
HMAC-SHA256 using `API_V2_ONBOARDING_SECRET`.
The canonical string that is signed is the newline-joined concatenation
of:
```
POST
<path>
<x-timestamp>
<x-nonce>
<raw request body>
```
where `<path>` is `/api/v2/automations/create/account`. The signature is
the lowercase hex digest.
On success, creates the user if they do not already exist, generates a
license key, emails it, and returns the user id. Invalid or missing
signatures return `401 { "error": "invalid signature" }` (the same
response is used for every signature failure, including replay and
timestamp skew).
operationId: createAutomationAccount
parameters:
- in: header
name: x-timestamp
required: true
schema:
type: string
description: Unix epoch seconds. Must be within 300 seconds of the server clock.
- in: header
name: x-nonce
required: true
schema:
type: string
description: Unique value per request. Replays within the skew window are rejected.
- in: header
name: x-signature
required: true
schema:
type: string
pattern: '^[0-9a-f]{64}$'
description: Lowercase hex HMAC-SHA256 of the canonical request string.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/OnboardingRequest'
responses:
'200':
description: Account and license key created.
content:
application/json:
schema:
$ref: '#/components/schemas/OnboardingResponse'
'400':
description: |
Malformed or invalid request. Possible messages:
`request body is required`, `invalid JSON body`, or
`email is required`.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
'401':
description: Signature missing, malformed, expired, replayed, or incorrect.
content:
application/json:
schema:
type: object
required: [error]
properties:
error:
type: string
example: invalid signature
'413':
description: The request body exceeded the 64 KiB limit.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
'429':
description: |
Rate limit exceeded. Limits are 5 requests per minute per IP and
3 requests per 24 hours per email address.
headers:
Retry-After:
schema:
type: integer
description: Seconds to wait before retrying.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
'500':
description: The account could not be created.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
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
Frequency:
type: object
required: [id, name, value]
properties:
id:
type: integer
minimum: 1
maximum: 9
description: Canonical id used by the kiosk.
name:
type: string
value:
type: integer
description: Frequency in Hz.
ColorPalette:
type: object
required: [solfeggioFrequencyId, colors]
properties:
solfeggioFrequencyId:
type: integer
minimum: 1
maximum: 9
colors:
type: array
description: Seven hex colors, ordered light to dark.
minItems: 1
items:
type: string
pattern: '^#[0-9A-Fa-f]{6}$'
Proclamation:
type: object
required: [id, name, content, categoryId, groupId, solfeggioFrequencyId]
properties:
id:
type: string
format: uuid
name:
type: string
content:
type: string
description: HTML content.
categoryId:
type: integer
groupId:
type: integer
solfeggioFrequencyId:
type: integer
minimum: 1
maximum: 9
colorPalette:
type: array
description: Present only when a palette exists for the canonical frequency id.
minItems: 1
items:
type: string
pattern: '^#[0-9A-Fa-f]{6}$'
LicenseRequest:
type: object
required: [license_key, email_address]
properties:
license_key:
type: string
pattern: '^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$'
example: ABCD-EFGH-IJKL-MNOP
email_address:
type: string
format: email
maxLength: 254
LicenseResponse:
type: object
required: [key, email_address]
properties:
key:
type: string
example: ABCD-EFGH-IJKL-MNOP
email_address:
type: string
format: email
OnboardingRequest:
type: object
required: [email]
properties:
email:
type: string
format: email
maxLength: 254
name:
type: string
maxLength: 120
description: Optional display name. Defaults to the email local-part.
OnboardingResponse:
type: object
required: [success, message, user_id]
properties:
success:
type: boolean
message:
type: string
example: Account and license key created
user_id:
type: string
ErrorMessage:
type: object
required: [message]
properties:
message:
type: string
+166
View File
@@ -0,0 +1,166 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light dark" />
<title>Verify your email address</title>
</head>
<body
style="
margin: 0;
padding: 0;
background-color: #f1f5f9;
-webkit-font-smoothing: antialiased;
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
"
>
<!-- Preheader (hidden) -->
<div style="display: none; max-height: 0; overflow: hidden; opacity: 0; color: transparent">
Confirm your email address to activate your Radiant Wave account.
</div>
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
style="background-color: #f1f5f9; padding: 32px 12px"
>
<tr>
<td align="center">
<table
role="presentation"
width="600"
cellpadding="0"
cellspacing="0"
style="
width: 100%;
max-width: 600px;
background-color: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 16px;
overflow: hidden;
"
>
<tr>
<td style="padding: 28px 32px 0 32px">
<span
style="
display: inline-block;
background-color: #4f46e5;
border-radius: 10px;
padding: 10px 14px;
color: #ffffff;
font-size: 16px;
font-weight: 700;
letter-spacing: -0.01em;
"
>
Radiant Wave
</span>
</td>
</tr>
<tr>
<td style="padding: 24px 32px 0 32px">
<h1
style="
margin: 0 0 12px 0;
color: #0f172a;
font-size: 24px;
line-height: 1.25;
font-weight: 700;
"
>
Confirm your email address
</h1>
<p style="margin: 0 0 8px 0; color: #334155; font-size: 16px; line-height: 1.6">
Thanks for signing up for Radiant Wave. Verify your email address to activate your
account.
</p>
</td>
</tr>
<tr>
<td style="padding: 16px 32px 0 32px">
<table role="presentation" cellpadding="0" cellspacing="0">
<tr>
<td style="border-radius: 10px; background-color: #4f46e5">
<a
href="{{{verification_link}}}"
target="_blank"
style="
display: inline-block;
padding: 14px 28px;
color: #ffffff;
font-size: 16px;
font-weight: 600;
text-decoration: none;
border-radius: 10px;
"
>
Verify Email Address
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding: 16px 32px 0 32px">
<p style="margin: 0; color: #64748b; font-size: 13px; line-height: 1.6">
If the button doesn't work, copy and paste this link into your browser:
</p>
<p
style="
margin: 6px 0 0 0;
word-break: break-all;
font-size: 13px;
line-height: 1.6;
"
>
<a
href="{{{verification_link}}}"
target="_blank"
style="color: #4f46e5; text-decoration: underline"
>{{{verification_link}}}</a
>
</p>
</td>
</tr>
<tr>
<td style="padding: 24px 32px 32px 32px">
<hr style="border: none; border-top: 1px solid #e2e8f0; margin: 0 0 16px 0" />
<p style="margin: 0; color: #94a3b8; font-size: 12px; line-height: 1.6">
This link expires in 1 hour. If you didn't create an account, you can safely
ignore this email.
</p>
</td>
</tr>
</table>
<table
role="presentation"
width="600"
cellpadding="0"
cellspacing="0"
style="width: 100%; max-width: 600px"
>
<tr>
<td style="padding: 20px 8px; text-align: center">
<p style="margin: 0; color: #94a3b8; font-size: 12px; line-height: 1.6">
Sent to {{{target_email_address}}}<br />
&copy; Radiant Wave Technologies
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
+184
View File
@@ -0,0 +1,184 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light dark" />
<title>Your Radiant Wave License Key</title>
</head>
<body
style="
margin: 0;
padding: 0;
background-color: #f1f5f9;
-webkit-font-smoothing: antialiased;
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
"
>
<!-- Preheader (hidden) -->
<div style="display: none; max-height: 0; overflow: hidden; opacity: 0; color: transparent">
Here's your Radiant Wave license key.
</div>
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
style="background-color: #f1f5f9; padding: 32px 12px"
>
<tr>
<td align="center">
<table
role="presentation"
width="600"
cellpadding="0"
cellspacing="0"
style="
width: 100%;
max-width: 600px;
background-color: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 16px;
overflow: hidden;
"
>
<tr>
<td style="padding: 28px 32px 0 32px">
<span
style="
display: inline-block;
background-color: #4f46e5;
border-radius: 10px;
padding: 10px 14px;
color: #ffffff;
font-size: 16px;
font-weight: 700;
letter-spacing: -0.01em;
"
>
Radiant Wave
</span>
</td>
</tr>
<tr>
<td style="padding: 24px 32px 0 32px">
<h1
style="
margin: 0 0 12px 0;
color: #0f172a;
font-size: 24px;
line-height: 1.25;
font-weight: 700;
"
>
Your license key
</h1>
<p style="margin: 0 0 8px 0; color: #334155; font-size: 16px; line-height: 1.6">
Hi {{{firstName}}}, your Radiant Wave account is ready. Here's your license key —
you'll need it to activate the app.
</p>
</td>
</tr>
<tr>
<td style="padding: 20px 32px 0 32px">
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
style="background-color: #f8fafc; border: 1px dashed #cbd5e1; border-radius: 12px"
>
<tr>
<td style="padding: 18px 20px; text-align: center">
<p
style="
margin: 0 0 6px 0;
color: #64748b;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
"
>
Your License Key
</p>
<p
style="
margin: 0;
color: #0f172a;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 20px;
font-weight: 700;
letter-spacing: 0.06em;
word-break: break-all;
"
>
{{{license_key}}}
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding: 20px 32px 0 32px">
<table role="presentation" cellpadding="0" cellspacing="0">
<tr>
<td style="border-radius: 10px; background-color: #4f46e5">
<a
href="https://portal.radiantwavetech.com/dashboard"
target="_blank"
style="
display: inline-block;
padding: 14px 28px;
color: #ffffff;
font-size: 16px;
font-weight: 600;
text-decoration: none;
border-radius: 10px;
"
>
Go to Your Dashboard
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding: 24px 32px 32px 32px">
<hr style="border: none; border-top: 1px solid #e2e8f0; margin: 0 0 16px 0" />
<p style="margin: 0; color: #94a3b8; font-size: 12px; line-height: 1.6">
Keep this key safe — anyone who has it can activate the app. If you didn't request
this, contact support.
</p>
</td>
</tr>
</table>
<table
role="presentation"
width="600"
cellpadding="0"
cellspacing="0"
style="width: 100%; max-width: 600px"
>
<tr>
<td style="padding: 20px 8px; text-align: center">
<p style="margin: 0; color: #94a3b8; font-size: 12px; line-height: 1.6">
&copy; Radiant Wave Technologies
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
+166
View File
@@ -0,0 +1,166 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light dark" />
<title>Reset your Radiant Wave password</title>
</head>
<body
style="
margin: 0;
padding: 0;
background-color: #f1f5f9;
-webkit-font-smoothing: antialiased;
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
"
>
<!-- Preheader (hidden) -->
<div style="display: none; max-height: 0; overflow: hidden; opacity: 0; color: transparent">
Choose a new password for your Radiant Wave account.
</div>
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
style="background-color: #f1f5f9; padding: 32px 12px"
>
<tr>
<td align="center">
<table
role="presentation"
width="600"
cellpadding="0"
cellspacing="0"
style="
width: 100%;
max-width: 600px;
background-color: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 16px;
overflow: hidden;
"
>
<tr>
<td style="padding: 28px 32px 0 32px">
<span
style="
display: inline-block;
background-color: #4f46e5;
border-radius: 10px;
padding: 10px 14px;
color: #ffffff;
font-size: 16px;
font-weight: 700;
letter-spacing: -0.01em;
"
>
Radiant Wave
</span>
</td>
</tr>
<tr>
<td style="padding: 24px 32px 0 32px">
<h1
style="
margin: 0 0 12px 0;
color: #0f172a;
font-size: 24px;
line-height: 1.25;
font-weight: 700;
"
>
Reset your password
</h1>
<p style="margin: 0 0 8px 0; color: #334155; font-size: 16px; line-height: 1.6">
We received a request to reset your Radiant Wave password. Click the button below
to choose a new one.
</p>
</td>
</tr>
<tr>
<td style="padding: 16px 32px 0 32px">
<table role="presentation" cellpadding="0" cellspacing="0">
<tr>
<td style="border-radius: 10px; background-color: #4f46e5">
<a
href="{{{reset_link}}}"
target="_blank"
style="
display: inline-block;
padding: 14px 28px;
color: #ffffff;
font-size: 16px;
font-weight: 600;
text-decoration: none;
border-radius: 10px;
"
>
Reset Password
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding: 16px 32px 0 32px">
<p style="margin: 0; color: #64748b; font-size: 13px; line-height: 1.6">
If the button doesn't work, copy and paste this link into your browser:
</p>
<p
style="
margin: 6px 0 0 0;
word-break: break-all;
font-size: 13px;
line-height: 1.6;
"
>
<a
href="{{{reset_link}}}"
target="_blank"
style="color: #4f46e5; text-decoration: underline"
>{{{reset_link}}}</a
>
</p>
</td>
</tr>
<tr>
<td style="padding: 24px 32px 32px 32px">
<hr style="border: none; border-top: 1px solid #e2e8f0; margin: 0 0 16px 0" />
<p style="margin: 0; color: #94a3b8; font-size: 12px; line-height: 1.6">
This link expires in 1 hour. If you didn't request a password reset, you can
safely ignore this email — your password won't change.
</p>
</td>
</tr>
</table>
<table
role="presentation"
width="600"
cellpadding="0"
cellspacing="0"
style="width: 100%; max-width: 600px"
>
<tr>
<td style="padding: 20px 8px; text-align: center">
<p style="margin: 0; color: #94a3b8; font-size: 12px; line-height: 1.6">
Sent to {{{target_email_address}}}<br />
&copy; Radiant Wave Technologies
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
+174
View File
@@ -0,0 +1,174 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light dark" />
<title>Welcome to Radiant Wave</title>
</head>
<body
style="
margin: 0;
padding: 0;
background-color: #f1f5f9;
-webkit-font-smoothing: antialiased;
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
"
>
<!-- Preheader (hidden) -->
<div style="display: none; max-height: 0; overflow: hidden; opacity: 0; color: transparent">
Set your password to finish setting up your Radiant Wave account.
</div>
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
style="background-color: #f1f5f9; padding: 32px 12px"
>
<tr>
<td align="center">
<table
role="presentation"
width="600"
cellpadding="0"
cellspacing="0"
style="
width: 100%;
max-width: 600px;
background-color: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 16px;
overflow: hidden;
"
>
<tr>
<td style="padding: 28px 32px 0 32px">
<span
style="
display: inline-block;
background-color: #4f46e5;
border-radius: 10px;
padding: 10px 14px;
color: #ffffff;
font-size: 16px;
font-weight: 700;
letter-spacing: -0.01em;
"
>
Radiant Wave
</span>
</td>
</tr>
<tr>
<td style="padding: 24px 32px 0 32px">
<h1
style="
margin: 0 0 12px 0;
color: #0f172a;
font-size: 24px;
line-height: 1.25;
font-weight: 700;
"
>
Welcome to Radiant Wave
</h1>
<p style="margin: 0 0 8px 0; color: #334155; font-size: 16px; line-height: 1.6">
An account has been created for you. Set a password to finish setting up your
account and access your dashboard.
</p>
</td>
</tr>
<tr>
<td style="padding: 16px 32px 0 32px">
<table role="presentation" cellpadding="0" cellspacing="0">
<tr>
<td style="border-radius: 10px; background-color: #4f46e5">
<a
href="{{{registration_link}}}"
target="_blank"
style="
display: inline-block;
padding: 14px 28px;
color: #ffffff;
font-size: 16px;
font-weight: 600;
text-decoration: none;
border-radius: 10px;
"
>
Set Your Password
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding: 16px 32px 0 32px">
<p style="margin: 0; color: #64748b; font-size: 13px; line-height: 1.6">
If the button doesn't work, copy and paste this link into your browser:
</p>
<p
style="
margin: 6px 0 0 0;
word-break: break-all;
font-size: 13px;
line-height: 1.6;
"
>
<a
href="{{{registration_link}}}"
target="_blank"
style="color: #4f46e5; text-decoration: underline"
>{{{registration_link}}}</a
>
</p>
</td>
</tr>
<tr>
<td style="padding: 24px 32px 0 32px">
<p style="margin: 0; color: #334155; font-size: 15px; line-height: 1.6">
After you set your password, sign in and complete your profile to get started.
</p>
</td>
</tr>
<tr>
<td style="padding: 24px 32px 32px 32px">
<hr style="border: none; border-top: 1px solid #e2e8f0; margin: 0 0 16px 0" />
<p style="margin: 0; color: #94a3b8; font-size: 12px; line-height: 1.6">
This link expires in 1 hour. If you didn't expect this email, you can safely
ignore it.
</p>
</td>
</tr>
</table>
<table
role="presentation"
width="600"
cellpadding="0"
cellspacing="0"
style="width: 100%; max-width: 600px"
>
<tr>
<td style="padding: 20px 8px; text-align: center">
<p style="margin: 0; color: #94a3b8; font-size: 12px; line-height: 1.6">
Sent to {{{target_email_address}}}<br />
&copy; Radiant Wave Technologies
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
+6
View File
@@ -37,5 +37,11 @@ export default defineConfig(
svelteConfig
}
}
},
{
files: ['**/*.spec.ts', '**/*.spec.js'],
rules: {
'@typescript-eslint/no-explicit-any': 'off'
}
}
);
+2 -2
View File
@@ -1,11 +1,11 @@
{
"name": "portal.Rhemawavetech.com",
"name": "portal.radiantwavetech.com",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "portal.Rhemawavetech.com",
"name": "portal.radiantwavetech.com",
"version": "0.0.1",
"dependencies": {
"@prisma/adapter-pg": "^7.8.0",
+2 -2
View File
@@ -1,5 +1,5 @@
{
"name": "portal.Rhemawavetech.com",
"name": "portal.radiantwavetech.com",
"private": true,
"version": "0.0.1",
"type": "module",
@@ -67,4 +67,4 @@
"sveltekit-superforms": "^2.30.2",
"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.
+9
View File
@@ -5,6 +5,15 @@ import { defineConfig } from 'prisma/config';
export default defineConfig({
schema: 'prisma/schema.prisma',
// Legacy subscription tables are retained in the database but are no longer
// managed by Prisma Migrate. They must never be dropped (production may not
// own them and the data is kept for recovery).
experimental: {
externalTables: true
},
tables: {
external: ['public.SubscriptionLevel', 'public.ProclamationCategorySubscriptionLevel']
},
migrations: {
path: 'prisma/migrations',
seed: 'tsx prisma/seed.ts'
@@ -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;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "user" ALTER COLUMN "roleId" SET DEFAULT 4;
@@ -0,0 +1,24 @@
-- Retain the legacy subscription artifacts as externally managed data.
--
-- SubscriptionLevel and ProclamationCategorySubscriptionLevel are no longer
-- modeled by the application. They are declared as external tables in
-- prisma.config.ts so Prisma Migrate ignores them entirely and never drops
-- them (production may not own these tables, and their data is kept for
-- recovery).
--
-- This migration only touches Prisma-managed tables to decouple them from the
-- legacy FK. It is intentionally idempotent so it applies cleanly to
-- production (columns present, NOT NULL, FK present), a fresh database built
-- from migration history, and development databases that already removed the
-- legacy objects.
-- Ensure the legacy columns exist. On databases built from migration history
-- these already exist; this re-adds them on environments that dropped them.
ALTER TABLE "Proclamation" ADD COLUMN IF NOT EXISTS "subscriptionLevelId" INTEGER;
ALTER TABLE "LicenseKey" ADD COLUMN IF NOT EXISTS "level" TEXT;
-- Decouple from SubscriptionLevel: drop the FK and relax NOT NULL so the
-- application can keep inserting rows without a legacy subscription value.
ALTER TABLE "Proclamation" DROP CONSTRAINT IF EXISTS "Proclamation_subscriptionLevelId_fkey";
ALTER TABLE "Proclamation" ALTER COLUMN "subscriptionLevelId" DROP NOT NULL;
ALTER TABLE "LicenseKey" ALTER COLUMN "level" DROP NOT NULL;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "LicenseKey" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "LicenseKey" ADD COLUMN "lastValidatedAt" TIMESTAMP(3);
@@ -0,0 +1,30 @@
-- CreateTable
CREATE TABLE "AuditLog" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userId" TEXT,
"email" TEXT,
"roleId" INTEGER,
"roleName" TEXT,
"page" TEXT NOT NULL,
"action" TEXT NOT NULL,
"entity" TEXT,
"entityId" TEXT,
"before" JSONB,
"after" JSONB,
"ip" TEXT,
CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "AuditLog_createdAt_idx" ON "AuditLog"("createdAt");
-- CreateIndex
CREATE INDEX "AuditLog_roleId_idx" ON "AuditLog"("roleId");
-- CreateIndex
CREATE INDEX "AuditLog_action_idx" ON "AuditLog"("action");
-- CreateIndex
CREATE INDEX "AuditLog_page_idx" ON "AuditLog"("page");
+98 -49
View File
@@ -21,11 +21,11 @@ model User {
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
roleId Int?
roleId Int? @default(4) // New users default to "User"
role UserRole? @relation(fields: [roleId], references: [id])
sessions Session[]
accounts Account[]
profile Profile?
profiles Profile[]
licenseKeys LicenseKey[]
userProclamations UserProclamations[]
@@ -37,28 +37,32 @@ model User {
// a bunch of time trying to figure out how to customize the default Better-Auth model.
model Profile {
id String @id @default(uuid())
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String?
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
email String?
firstName String
lastName String
phoneNumber String
ghlContactId String? @map("ghl_contact_id")
addresses Address[]
@@index([userId])
@@index([email])
}
model Address {
id String @id @default(uuid())
profileId String
profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade)
street String
city String
state String
postalCode String
country String
type String @default("Mailing")
isDefaultMailing Boolean @default(false)
isDefaultBilling Boolean @default(false)
isDefaultShipping Boolean @default(false)
id String @id @default(uuid())
profileId String
profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade)
street String
city String
state String
postalCode String
country String
type String @default("Mailing")
isDefaultMailing Boolean @default(false)
isDefaultBilling Boolean @default(false)
isDefaultShipping Boolean @default(false)
}
model Session {
@@ -123,9 +127,19 @@ model Proclamation {
solfeggioFrequency SolfeggioFrequency @relation(fields: [solfeggioFrequencyId], references: [id])
categoryId Int
category ProclamationCategory @relation(fields: [categoryId], references: [id])
groupId Int
group ProclamationGroup @relation(fields: [groupId], references: [id])
userProclamations UserProclamations[]
subscriptionLevelId Int // Keeping old FK for migration stability
subscriptionLevel SubscriptionLevel @relation(fields: [subscriptionLevelId], references: [id])
// Legacy subscription-level FK. Retained in the database but unmanaged; the
// FK constraint is dropped and the value is left NULL. Hidden from the client.
subscriptionLevelId Int? @ignore
}
model ProclamationGroup {
id Int @id @default(autoincrement())
name String @unique
sortOrder Int @default(0)
proclamations Proclamation[]
}
model UserProclamations {
@@ -142,40 +156,59 @@ model UserProclamations {
model SolfeggioFrequency {
id Int @id @default(autoincrement())
name String @unique
value Int @unique
value Int @unique
proclamations Proclamation[]
}
model ProclamationCategory {
id Int @id @default(autoincrement())
name String
sortOrder Int @default(0)
parentId Int?
parent ProclamationCategory? @relation("CategoryHierarchy", fields: [parentId], references: [id])
children ProclamationCategory[] @relation("CategoryHierarchy")
proclamations Proclamation[]
subscriptionLevels ProclamationCategorySubscriptionLevel[]
@@unique([parentId, name])
id Int @id @default(autoincrement())
name String @unique
sortOrder Int @default(0)
proclamations Proclamation[]
}
// Join table for ProclamationCategory -> SubscriptionLevel defaults
model ProclamationCategorySubscriptionLevel {
categoryId Int
category ProclamationCategory @relation(fields: [categoryId], references: [id], onDelete: Cascade)
// Archive of the former child sub-categories removed when the
// ProclamationCategory hierarchy was flattened. Kept for recovery/audit.
// `@@ignore` keeps it out of the generated client while Prisma remains aware
// 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
subscriptionLevel SubscriptionLevel @relation(fields: [subscriptionLevelId], references: [id], onDelete: Cascade)
@@id([categoryId, subscriptionLevelId])
@@map("ProclamationCategorySubscriptionLevelLegacy")
@@ignore
}
model LicenseKey {
id String @id @default(uuid())
key String @unique
active Boolean
level String
userId String
user User @relation(fields: [userId], references: [id])
id String @id @default(uuid())
key String @unique
active Boolean
createdAt DateTime @default(now())
lastValidatedAt DateTime?
userId String?
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
email String?
// Legacy subscription level. Retained in the database but unmanaged; hidden
// from the client and left NULL.
level String? @ignore
@@index([userId])
@@index([email])
}
model ApiKey {
@@ -187,14 +220,6 @@ model ApiKey {
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).
// `solfeggioFrequencyId` is the canonical id the kiosk uses (see
// v2-serializers.ts), not the DB autoincrement id of `SolfeggioFrequency`.
@@ -203,4 +228,28 @@ model ColorPalette {
colors Json // ["#RRGGBB", ... x7]
@@map("ColorPalette")
}
}
// Append-only audit trail of database mutations. Written centrally by the
// Prisma extension in src/lib/db.ts; read by the admin Audit Log page.
model AuditLog {
id String @id @default(uuid())
createdAt DateTime @default(now())
userId String?
email String?
roleId Int?
roleName String?
page String
action String
entity String?
entityId String?
before Json?
after Json?
ip String?
@@index([createdAt])
@@index([roleId])
@@index([action])
@@index([page])
@@map("AuditLog")
}
+26 -43
View File
@@ -72,43 +72,26 @@ const colorPalettes: { solfeggioFrequencyId: number; colors: string[] }[] = [
}
];
const subscriptionLevels = [
{ id: 1, name: 'Basic', description: 'General only' },
{ id: 2, name: 'Advanced', description: 'General + Specific' },
{ id: 3, name: 'Elite', description: 'General + Specific + Precise' }
const proclamationGroups = [
{ name: 'General', sortOrder: 0 },
{ name: 'Specific', sortOrder: 1 },
{ name: 'Precise', sortOrder: 2 },
{ name: 'Prayers', sortOrder: 3 },
{ name: 'Scripture', sortOrder: 4 }
];
// Flat category list (formerly hierarchical parents).
const proclamationCategories = [
{ id: 4, name: 'Spiritual', parentId: null },
{ id: 5, name: 'General', parentId: 4 },
{ id: 9, name: 'Specific', parentId: 4 },
{ id: 40, name: 'Precise', parentId: 4 },
{ id: 2, name: 'Physical', parentId: null },
{ id: 3, name: 'General', parentId: 2 },
{ id: 10, name: 'Specific', parentId: 2 },
{ id: 32, name: 'Precise', parentId: 2 },
{ id: 16, name: 'Mental', parentId: null },
{ id: 30, name: 'General', parentId: 16 },
{ 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 }
{ id: 4, name: 'Spiritual' },
{ id: 2, name: 'Physical' },
{ id: 16, name: 'Mental' },
{ id: 17, name: 'Custom (Client Specific)' },
{ id: 19, name: 'Financial' },
{ id: 20, name: 'Emotional' },
{ id: 21, name: 'Social' },
{ id: 41, name: 'Biblical' },
{ id: 42, name: 'Personal Growth & Development' },
{ id: 46, name: 'Sports' }
];
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...');
for (const cat of proclamationCategories) {
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...');
for (const palette of colorPalettes) {
await prisma.colorPalette.upsert({
+3 -3
View File
@@ -1,11 +1,11 @@
import type { Session, User } from 'better-auth';
import type { auth } from '$lib/auth';
import type { ApiKeyRecord } from '$lib/server/api-key';
declare global {
namespace App {
interface Locals {
session: Session | null;
user: User | null;
session: typeof auth.$Infer.Session.session | null;
user: typeof auth.$Infer.Session.user | null;
apiKey?: ApiKeyRecord;
}
}
+10 -10
View File
@@ -5,21 +5,21 @@
<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');
try {
const theme = localStorage.getItem('theme');
const dark =
theme === 'dark' ||
(!theme && window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.classList.toggle('dark', dark);
} catch (e) {
// ignore
}
})();
</script>
%sveltekit.head%
</head>
<body class="m-0" data-sveltekit-preload-data="hover">
<div class="flex h-dvh w-dvw flex-col">%sveltekit.body%</div>
<body data-sveltekit-preload-data="hover">
<div>%sveltekit.body%</div>
</body>
</html>
+79 -58
View File
@@ -3,71 +3,92 @@ import { svelteKitHandler } from 'better-auth/svelte-kit';
import { building } from '$app/environment';
import { env } from '$env/dynamic/private';
import { validateApiKey } from '$lib/server/api-key';
import { runWithAuditContext, type AuditContext } from '$lib/server/audit-context';
import { clientIp } from '$lib/server/v2-helpers';
function contextFor(
event: { url: URL; request: Request },
session?: App.Locals['user']
): AuditContext {
return {
userId: session?.id ?? null,
email: session?.email ?? null,
roleId: session?.roleId ?? null,
page: event.url.pathname,
ip: clientIp(event.request)
};
}
export async function handle({ event, resolve }) {
// v2 auth is ALWAYS enforced — no API_KEYS_ACTIVE bypass. The only
// exceptions are the signed onboarding routes (HMAC-protected).
// TODO: re-enable when v2 API key auth is ready.
// const isV2 = event.url.pathname.startsWith('/api/v2/');
// const isSignedOnboarding = event.url.pathname.startsWith('/api/v2/automations/');
//
// if (isV2 && !isSignedOnboarding) {
// 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 key = authHeader.slice(7);
// const record = await validateApiKey(key);
// if (!record) {
// return new Response(JSON.stringify({ error: 'Invalid or expired API key' }), {
// status: 401,
// headers: { 'content-type': 'application/json' }
// });
// }
//
// event.locals.apiKey = record;
//
// return resolve(event);
// }
return runWithAuditContext(contextFor(event), async () => {
// v2 auth is ALWAYS enforced — no API_KEYS_ACTIVE bypass. The only
// exceptions are the signed onboarding routes (HMAC-protected).
// TODO: re-enable when v2 API key auth is ready.
// const isV2 = event.url.pathname.startsWith('/api/v2/');
// const isSignedOnboarding = event.url.pathname.startsWith('/api/v2/automations/');
//
// if (isV2 && !isSignedOnboarding) {
// 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 key = authHeader.slice(7);
// const record = await validateApiKey(key);
// if (!record) {
// return new Response(JSON.stringify({ error: 'Invalid or expired API key' }), {
// status: 401,
// headers: { 'content-type': 'application/json' }
// });
// }
//
// event.locals.apiKey = record;
//
// return resolve(event);
// }
if (
event.url.pathname.startsWith('/api/v1/') &&
!event.url.pathname.startsWith('/api/v1/automations/')
) {
if (env.API_KEYS_ACTIVE !== 'false') {
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' }
});
if (
event.url.pathname.startsWith('/api/v1/') &&
!event.url.pathname.startsWith('/api/v1/automations/')
) {
if (env.API_KEYS_ACTIVE !== 'false') {
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 key = authHeader.slice(7);
const record = await validateApiKey(key);
if (!record) {
return new Response(JSON.stringify({ error: 'Invalid or expired API key' }), {
status: 401,
headers: { 'content-type': 'application/json' }
});
}
}
const key = authHeader.slice(7);
const record = await validateApiKey(key);
if (!record) {
return new Response(JSON.stringify({ error: 'Invalid or expired API key' }), {
status: 401,
headers: { 'content-type': 'application/json' }
});
}
return resolve(event);
}
return resolve(event);
}
const session = await auth.api.getSession({
headers: event.request.headers
});
const session = await auth.api.getSession({
headers: event.request.headers
if (session) {
event.locals.session = session.session;
event.locals.user = session.user;
return runWithAuditContext(contextFor(event, session.user), () =>
svelteKitHandler({ event, resolve, auth, building })
);
}
return svelteKitHandler({ event, resolve, auth, building });
});
if (session) {
event.locals.session = session.session;
event.locals.user = session.user;
}
return svelteKitHandler({ event, resolve, auth, building });
}
+36 -1
View File
@@ -5,13 +5,40 @@ import { sveltekitCookies } from 'better-auth/svelte-kit';
import { getRequestEvent } from '$app/server';
import { sendMail } from './server/resend';
import { sendVerificationEmail } from './server/email-verification';
import { sendRegistrationEmail } from './server/registration-email';
import { env } from '$env/dynamic/private';
/**
* An admin-created user is invited with a set-password link. That reuses the
* password-reset token flow but marks the request with `invite=1` so we send a
* welcome/registration email instead of a "you requested a reset" email.
*/
function isRegistrationInvite(url: string): boolean {
try {
const callbackURL = new URL(url).searchParams.get('callbackURL') ?? '';
return callbackURL.includes('invite=1');
} catch {
return false;
}
}
export const auth = betterAuth({
baseURL: env.BETTER_AUTH_URL,
database: prismaAdapter(prisma, {
provider: 'postgresql'
}),
user: {
// Expose our custom role column on the session user so role checks can
// read `locals.user.roleId` without a database round-trip.
additionalFields: {
roleId: {
type: 'number',
required: false,
defaultValue: 4,
input: false
}
}
},
emailVerification: {
sendOnSignUp: true,
sendVerificationEmail: async ({ user, token }) => {
@@ -27,8 +54,16 @@ export const auth = betterAuth({
enabled: true,
requireEmailVerification: true,
sendResetPassword: async ({ user, url }) => {
if (isRegistrationInvite(url)) {
void sendRegistrationEmail({ email: user.email, url }).catch((e) => {
console.error('sendRegistrationEmail template email failed:', e);
});
return;
}
void sendMail({
to: user.email,
subject: 'Reset your Radiant Wave password',
templateId: 'password-reset-email',
data: {
target_email_address: user.email,
@@ -51,6 +86,6 @@ export const auth = betterAuth({
window: 60,
max: 100
},
trustedOrigins: ['http://localhost:5173', 'https://portal.rhemawavetech.com'],
trustedOrigins: ['http://localhost:5173', 'https://portal.radiantwavetech.com'],
plugins: [sveltekitCookies(getRequestEvent)]
});
@@ -0,0 +1,25 @@
<script lang="ts">
import type { Snippet } from 'svelte';
let {
title,
description = '',
children
}: {
title: string;
description?: string;
children: Snippet;
} = $props();
</script>
<section class="mt-8">
<div>
<h2 class="text-lg font-semibold text-slate-900 dark:text-slate-100">{title}</h2>
{#if description}
<p class="mt-1 text-sm text-slate-500 dark:text-slate-400">{description}</p>
{/if}
</div>
<div class="mt-4">
{@render children()}
</div>
</section>
+219
View File
@@ -0,0 +1,219 @@
<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 dark:text-slate-100">{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 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
/>
{#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 dark:border-slate-800 dark:bg-slate-900"
>
<table class="w-full min-w-max text-left text-sm text-slate-700 dark:text-slate-300">
<thead>
<tr
class="border-b border-slate-200 bg-slate-100 text-xs font-semibold tracking-wide text-slate-500 dark:border-slate-800 dark:bg-slate-800 dark:text-slate-400"
>
{#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 dark:bg-slate-900'
: 'bg-slate-50 dark:bg-slate-950'} {onRowClick
? 'cursor-pointer hover:bg-indigo-50 dark:hover:bg-indigo-950'
: ''}"
{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 dark:text-slate-500"
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 dark:text-slate-500 dark:hover:text-indigo-400"
>
<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 dark:bg-slate-900 px-4 py-10 text-center text-slate-500 dark:text-slate-400"
>
No results found.
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
+17 -12
View File
@@ -17,8 +17,8 @@
deleteAction?: string;
} = $props();
function handleBackdrop(e: MouseEvent) {
if (e.target === e.currentTarget) onClose?.();
function handleBackdrop() {
onClose?.();
}
function handleKeydown(e: KeyboardEvent) {
@@ -44,24 +44,27 @@
{#if open}
<div
role="presentation"
class="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/50 pt-12"
onclick={handleBackdrop}
class="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/50 p-4"
onkeydown={handleKeydown}
role="presentation"
>
<div class="absolute inset-0" onclick={handleBackdrop} aria-hidden="true"></div>
<div
class="my-8 w-full max-w-2xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900"
role="dialog"
aria-modal="true"
aria-label={title}
class="relative z-10 flex max-h-[90vh] w-full max-w-2xl flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-xl dark:border-slate-800 dark:bg-slate-900"
>
<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 dark:border-slate-800"
>
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">{title}</h2>
<h2 class="text-lg font-bold text-slate-900 dark:text-slate-100">{title}</h2>
<div class="flex items-center gap-2">
{#if canDelete && recordId}
<button
type="button"
onclick={confirmDelete}
class="px-3 py-1 text-sm text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
class="rounded-md bg-red-50 px-3 py-1.5 text-sm font-medium text-red-600 ring-1 ring-red-200 transition ring-inset hover:bg-red-600 hover:text-white dark:bg-red-950/50 dark:text-red-400 dark:ring-red-900"
>
Delete
</button>
@@ -69,12 +72,14 @@
<button
type="button"
onclick={onClose}
class="px-2 py-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>&times;</button
aria-label="Close"
class="rounded-md p-1.5 text-2xl leading-none text-slate-400 transition hover:bg-slate-100 hover:text-red-600 dark:text-slate-500 dark:hover:bg-slate-800"
>
&times;
</button>
</div>
</div>
<div class="px-6 py-4">
<div class="overflow-y-auto px-6 py-5">
{@render children()}
</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">
let {
message = '',
type = 'success' as 'success' | 'error'
}: {
message?: string;
type?: 'success' | 'error';
} = $props();
let { message = '' }: { message?: string } = $props();
let remaining = $state(5);
let visible = $state(true);
@@ -32,15 +26,17 @@
{#if visible}
<div
role="alert"
class="fixed bottom-4 left-4 z-50 flex min-w-64 items-center gap-3 border px-4 py-3 text-sm shadow-lg {type ===
'success'
? 'border-green-600 bg-green-50 text-green-800'
: 'border-red-600 bg-red-50 text-red-800'}"
class="fixed right-4 bottom-4 z-50 flex items-center gap-3 rounded-lg border border-slate-700 bg-slate-900 px-4 py-3 text-sm text-white shadow-lg dark:border-slate-300 dark:bg-slate-950"
>
<span class="flex-1">{message}</span>
<span class="text-xs tabular-nums opacity-70">{remaining}s</span>
<button type="button" onclick={dismiss} class="ml-1 leading-none font-bold hover:opacity-70"
>&times;</button
<span>{message}</span>
<span class="text-xs text-slate-400 dark:text-slate-500">{remaining}s</span>
<button
type="button"
onclick={dismiss}
aria-label="Dismiss"
class="rounded p-1 text-slate-400 transition hover:text-white dark:text-slate-500"
>
&times;
</button>
</div>
{/if}
+6 -2
View File
@@ -2,6 +2,10 @@
let { children } = $props();
</script>
<div class="flex min-h-0 flex-1 items-center justify-center overflow-auto">
{@render children()}
<div class="mx-auto w-full max-w-md px-4 py-8 sm:py-12">
<div
class="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8 dark:border-slate-800 dark:bg-slate-900"
>
{@render children()}
</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} />
+161 -7
View File
@@ -2,18 +2,172 @@ import pg from 'pg';
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '$lib/generated/prisma/client';
import { env } from '$env/dynamic/private';
import { getAuditContext } from '$lib/server/audit-context';
import { describeAction, entityIdFor, sanitizeAuditValue, shouldAudit } from '$lib/server/audit';
let _prisma: PrismaClient | undefined;
let _base: PrismaClient | undefined;
let _pool: pg.Pool | undefined;
function getBase(): { base: PrismaClient; pool: pg.Pool } {
if (!_base || !_pool) {
_pool = new pg.Pool({ connectionString: env.DATABASE_URL });
const adapter = new PrismaPg(_pool);
_base = new PrismaClient({ adapter });
}
return { base: _base, pool: _pool };
}
const roleNameCache = new Map<number, string>();
async function resolveRoleName(base: PrismaClient, roleId: number | null): Promise<string | null> {
if (roleId === null) return null;
const cached = roleNameCache.get(roleId);
if (cached) return cached;
try {
const role = await base.userRole.findUnique({
where: { id: roleId },
select: { name: true }
});
if (role?.name) {
roleNameCache.set(roleId, role.name);
return role.name;
}
} catch (e) {
console.error('Failed to resolve role name for audit log:', e);
}
return null;
}
type Delegate = {
findFirst?: (args: unknown) => Promise<unknown>;
findMany?: (args: unknown) => Promise<unknown>;
};
function delegateFor(base: PrismaClient, model: string): Delegate | undefined {
const key = model.charAt(0).toLowerCase() + model.slice(1);
return (base as unknown as Record<string, unknown>)[key] as Delegate | undefined;
}
async function readBefore(
base: PrismaClient,
model: string,
operation: string,
args: unknown
): Promise<unknown> {
if (operation.startsWith('create')) return null;
const delegate = delegateFor(base, model);
if (!delegate) return null;
const where = (args as { where?: unknown } | null)?.where;
try {
if (operation === 'updateMany' || operation === 'deleteMany') {
return delegate.findMany ? await delegate.findMany({ where, take: 50 }) : null;
}
return delegate.findFirst ? await delegate.findFirst({ where }) : null;
} catch {
return null;
}
}
async function writeAuditRow(
pool: pg.Pool,
base: PrismaClient,
entry: {
userId: string | null;
email: string | null;
roleId: number | null;
page: string;
action: string;
entity: string;
entityId: string | null;
before: unknown;
after: unknown;
ip: string | null;
}
): Promise<void> {
const roleName = await resolveRoleName(base, entry.roleId);
await pool.query(
`INSERT INTO "AuditLog"
("id", "createdAt", "userId", "email", "roleId", "roleName", "page", "action", "entity", "entityId", "before", "after", "ip")
VALUES ($1, now(), $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb, $11::jsonb, $12)`,
[
crypto.randomUUID(),
entry.userId,
entry.email,
entry.roleId,
roleName,
entry.page,
entry.action,
entry.entity,
entry.entityId,
entry.before === null ? null : JSON.stringify(entry.before),
entry.after === null ? null : JSON.stringify(entry.after),
entry.ip
]
);
}
function getPrisma(): PrismaClient {
if (!_prisma) {
const pool = new pg.Pool({
connectionString: env.DATABASE_URL
});
if (_prisma) return _prisma;
const adapter = new PrismaPg(pool);
_prisma = new PrismaClient({ adapter });
}
const { base, pool } = getBase();
const extended = base.$extends({
query: {
$allModels: {
async $allOperations({ model, operation, args, query }) {
if (!shouldAudit(model, operation)) {
return query(args);
}
const before = await readBefore(base, model, operation, args);
const result = await query(args);
try {
const ctx = getAuditContext();
const res = result as { id?: unknown; email?: string; roleId?: number | null } | null;
let userId = ctx?.userId ?? null;
let email = ctx?.email ?? null;
let roleId = ctx?.roleId ?? null;
// Self-signup: attribute the new user to itself since no session exists yet.
if (!userId && model === 'User' && operation === 'create' && res) {
userId = (res.id as string | undefined) ?? null;
email = res.email ?? null;
roleId = res.roleId ?? null;
}
const after =
operation === 'updateMany' || operation === 'deleteMany'
? { count: (result as { count?: number } | null)?.count ?? null }
: operation.startsWith('delete')
? null
: result;
await writeAuditRow(pool, base, {
userId,
email,
roleId,
page: ctx?.page ?? '/system',
action: describeAction(operation),
entity: model,
entityId: entityIdFor(args, result, before),
before: sanitizeAuditValue(before),
after: sanitizeAuditValue(after),
ip: ctx?.ip ?? null
});
} catch (e) {
console.error('Failed to write audit log:', e);
}
return result;
}
}
}
});
_prisma = extended as unknown as PrismaClient;
return _prisma;
}
+10 -10
View File
@@ -57,6 +57,11 @@ export type Verification = Prisma.VerificationModel
*
*/
export type Proclamation = Prisma.ProclamationModel
/**
* Model ProclamationGroup
*
*/
export type ProclamationGroup = Prisma.ProclamationGroupModel
/**
* Model UserProclamations
*
@@ -72,11 +77,6 @@ export type SolfeggioFrequency = Prisma.SolfeggioFrequencyModel
*
*/
export type ProclamationCategory = Prisma.ProclamationCategoryModel
/**
* Model ProclamationCategorySubscriptionLevel
*
*/
export type ProclamationCategorySubscriptionLevel = Prisma.ProclamationCategorySubscriptionLevelModel
/**
* Model LicenseKey
*
@@ -87,13 +87,13 @@ export type LicenseKey = Prisma.LicenseKeyModel
*
*/
export type ApiKey = Prisma.ApiKeyModel
/**
* Model SubscriptionLevel
*
*/
export type SubscriptionLevel = Prisma.SubscriptionLevelModel
/**
* Model ColorPalette
*
*/
export type ColorPalette = Prisma.ColorPaletteModel
/**
* Model AuditLog
*
*/
export type AuditLog = Prisma.AuditLogModel
+10 -10
View File
@@ -81,6 +81,11 @@ export type Verification = Prisma.VerificationModel
*
*/
export type Proclamation = Prisma.ProclamationModel
/**
* Model ProclamationGroup
*
*/
export type ProclamationGroup = Prisma.ProclamationGroupModel
/**
* Model UserProclamations
*
@@ -96,11 +101,6 @@ export type SolfeggioFrequency = Prisma.SolfeggioFrequencyModel
*
*/
export type ProclamationCategory = Prisma.ProclamationCategoryModel
/**
* Model ProclamationCategorySubscriptionLevel
*
*/
export type ProclamationCategorySubscriptionLevel = Prisma.ProclamationCategorySubscriptionLevelModel
/**
* Model LicenseKey
*
@@ -111,13 +111,13 @@ export type LicenseKey = Prisma.LicenseKeyModel
*
*/
export type ApiKey = Prisma.ApiKeyModel
/**
* Model SubscriptionLevel
*
*/
export type SubscriptionLevel = Prisma.SubscriptionLevelModel
/**
* Model ColorPalette
*
*/
export type ColorPalette = Prisma.ColorPaletteModel
/**
* Model AuditLog
*
*/
export type AuditLog = Prisma.AuditLogModel
@@ -253,6 +253,57 @@ export type JsonWithAggregatesFilterBase<$PrismaModel = never> = {
_max?: Prisma.NestedJsonFilter<$PrismaModel>
}
export type JsonNullableFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<JsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>,
Required<JsonNullableFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>
export type JsonNullableFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}
export type JsonNullableWithAggregatesFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>,
Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>
export type JsonNullableWithAggregatesFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedJsonNullableFilter<$PrismaModel>
_max?: Prisma.NestedJsonNullableFilter<$PrismaModel>
}
export type NestedStringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
@@ -478,4 +529,28 @@ export type NestedJsonFilterBase<$PrismaModel = never> = {
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}
export type NestedJsonNullableFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<NestedJsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>,
Required<NestedJsonNullableFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>
export type NestedJsonNullableFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}
File diff suppressed because one or more lines are too long
@@ -392,14 +392,14 @@ export const ModelName = {
UserRole: 'UserRole',
Verification: 'Verification',
Proclamation: 'Proclamation',
ProclamationGroup: 'ProclamationGroup',
UserProclamations: 'UserProclamations',
SolfeggioFrequency: 'SolfeggioFrequency',
ProclamationCategory: 'ProclamationCategory',
ProclamationCategorySubscriptionLevel: 'ProclamationCategorySubscriptionLevel',
LicenseKey: 'LicenseKey',
ApiKey: 'ApiKey',
SubscriptionLevel: 'SubscriptionLevel',
ColorPalette: 'ColorPalette'
ColorPalette: 'ColorPalette',
AuditLog: 'AuditLog'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
@@ -415,7 +415,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
omit: GlobalOmitOptions
}
meta: {
modelProps: "user" | "profile" | "address" | "session" | "account" | "userRole" | "verification" | "proclamation" | "userProclamations" | "solfeggioFrequency" | "proclamationCategory" | "proclamationCategorySubscriptionLevel" | "licenseKey" | "apiKey" | "subscriptionLevel" | "colorPalette"
modelProps: "user" | "profile" | "address" | "session" | "account" | "userRole" | "verification" | "proclamation" | "proclamationGroup" | "userProclamations" | "solfeggioFrequency" | "proclamationCategory" | "licenseKey" | "apiKey" | "colorPalette" | "auditLog"
txIsolationLevel: TransactionIsolationLevel
}
model: {
@@ -1011,6 +1011,80 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
}
}
}
ProclamationGroup: {
payload: Prisma.$ProclamationGroupPayload<ExtArgs>
fields: Prisma.ProclamationGroupFieldRefs
operations: {
findUnique: {
args: Prisma.ProclamationGroupFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload> | null
}
findUniqueOrThrow: {
args: Prisma.ProclamationGroupFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>
}
findFirst: {
args: Prisma.ProclamationGroupFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload> | null
}
findFirstOrThrow: {
args: Prisma.ProclamationGroupFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>
}
findMany: {
args: Prisma.ProclamationGroupFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>[]
}
create: {
args: Prisma.ProclamationGroupCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>
}
createMany: {
args: Prisma.ProclamationGroupCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.ProclamationGroupCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>[]
}
delete: {
args: Prisma.ProclamationGroupDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>
}
update: {
args: Prisma.ProclamationGroupUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>
}
deleteMany: {
args: Prisma.ProclamationGroupDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.ProclamationGroupUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.ProclamationGroupUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>[]
}
upsert: {
args: Prisma.ProclamationGroupUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>
}
aggregate: {
args: Prisma.ProclamationGroupAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateProclamationGroup>
}
groupBy: {
args: Prisma.ProclamationGroupGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.ProclamationGroupGroupByOutputType>[]
}
count: {
args: Prisma.ProclamationGroupCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.ProclamationGroupCountAggregateOutputType> | number
}
}
}
UserProclamations: {
payload: Prisma.$UserProclamationsPayload<ExtArgs>
fields: Prisma.UserProclamationsFieldRefs
@@ -1233,80 +1307,6 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
}
}
}
ProclamationCategorySubscriptionLevel: {
payload: Prisma.$ProclamationCategorySubscriptionLevelPayload<ExtArgs>
fields: Prisma.ProclamationCategorySubscriptionLevelFieldRefs
operations: {
findUnique: {
args: Prisma.ProclamationCategorySubscriptionLevelFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload> | null
}
findUniqueOrThrow: {
args: Prisma.ProclamationCategorySubscriptionLevelFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>
}
findFirst: {
args: Prisma.ProclamationCategorySubscriptionLevelFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload> | null
}
findFirstOrThrow: {
args: Prisma.ProclamationCategorySubscriptionLevelFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>
}
findMany: {
args: Prisma.ProclamationCategorySubscriptionLevelFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>[]
}
create: {
args: Prisma.ProclamationCategorySubscriptionLevelCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>
}
createMany: {
args: Prisma.ProclamationCategorySubscriptionLevelCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.ProclamationCategorySubscriptionLevelCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>[]
}
delete: {
args: Prisma.ProclamationCategorySubscriptionLevelDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>
}
update: {
args: Prisma.ProclamationCategorySubscriptionLevelUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>
}
deleteMany: {
args: Prisma.ProclamationCategorySubscriptionLevelDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.ProclamationCategorySubscriptionLevelUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.ProclamationCategorySubscriptionLevelUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>[]
}
upsert: {
args: Prisma.ProclamationCategorySubscriptionLevelUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>
}
aggregate: {
args: Prisma.ProclamationCategorySubscriptionLevelAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateProclamationCategorySubscriptionLevel>
}
groupBy: {
args: Prisma.ProclamationCategorySubscriptionLevelGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.ProclamationCategorySubscriptionLevelGroupByOutputType>[]
}
count: {
args: Prisma.ProclamationCategorySubscriptionLevelCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.ProclamationCategorySubscriptionLevelCountAggregateOutputType> | number
}
}
}
LicenseKey: {
payload: Prisma.$LicenseKeyPayload<ExtArgs>
fields: Prisma.LicenseKeyFieldRefs
@@ -1455,80 +1455,6 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
}
}
}
SubscriptionLevel: {
payload: Prisma.$SubscriptionLevelPayload<ExtArgs>
fields: Prisma.SubscriptionLevelFieldRefs
operations: {
findUnique: {
args: Prisma.SubscriptionLevelFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload> | null
}
findUniqueOrThrow: {
args: Prisma.SubscriptionLevelFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>
}
findFirst: {
args: Prisma.SubscriptionLevelFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload> | null
}
findFirstOrThrow: {
args: Prisma.SubscriptionLevelFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>
}
findMany: {
args: Prisma.SubscriptionLevelFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>[]
}
create: {
args: Prisma.SubscriptionLevelCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>
}
createMany: {
args: Prisma.SubscriptionLevelCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.SubscriptionLevelCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>[]
}
delete: {
args: Prisma.SubscriptionLevelDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>
}
update: {
args: Prisma.SubscriptionLevelUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>
}
deleteMany: {
args: Prisma.SubscriptionLevelDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.SubscriptionLevelUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.SubscriptionLevelUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>[]
}
upsert: {
args: Prisma.SubscriptionLevelUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>
}
aggregate: {
args: Prisma.SubscriptionLevelAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateSubscriptionLevel>
}
groupBy: {
args: Prisma.SubscriptionLevelGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.SubscriptionLevelGroupByOutputType>[]
}
count: {
args: Prisma.SubscriptionLevelCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.SubscriptionLevelCountAggregateOutputType> | number
}
}
}
ColorPalette: {
payload: Prisma.$ColorPalettePayload<ExtArgs>
fields: Prisma.ColorPaletteFieldRefs
@@ -1603,6 +1529,80 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
}
}
}
AuditLog: {
payload: Prisma.$AuditLogPayload<ExtArgs>
fields: Prisma.AuditLogFieldRefs
operations: {
findUnique: {
args: Prisma.AuditLogFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AuditLogPayload> | null
}
findUniqueOrThrow: {
args: Prisma.AuditLogFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AuditLogPayload>
}
findFirst: {
args: Prisma.AuditLogFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AuditLogPayload> | null
}
findFirstOrThrow: {
args: Prisma.AuditLogFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AuditLogPayload>
}
findMany: {
args: Prisma.AuditLogFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AuditLogPayload>[]
}
create: {
args: Prisma.AuditLogCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AuditLogPayload>
}
createMany: {
args: Prisma.AuditLogCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.AuditLogCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AuditLogPayload>[]
}
delete: {
args: Prisma.AuditLogDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AuditLogPayload>
}
update: {
args: Prisma.AuditLogUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AuditLogPayload>
}
deleteMany: {
args: Prisma.AuditLogDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.AuditLogUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.AuditLogUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AuditLogPayload>[]
}
upsert: {
args: Prisma.AuditLogUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AuditLogPayload>
}
aggregate: {
args: Prisma.AuditLogAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateAuditLog>
}
groupBy: {
args: Prisma.AuditLogGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AuditLogGroupByOutputType>[]
}
count: {
args: Prisma.AuditLogCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AuditLogCountAggregateOutputType> | number
}
}
}
}
} & {
other: {
@@ -1659,6 +1659,7 @@ export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof User
export const ProfileScalarFieldEnum = {
id: 'id',
userId: 'userId',
email: 'email',
firstName: 'firstName',
lastName: 'lastName',
phoneNumber: 'phoneNumber',
@@ -1744,12 +1745,21 @@ export const ProclamationScalarFieldEnum = {
content: 'content',
solfeggioFrequencyId: 'solfeggioFrequencyId',
categoryId: 'categoryId',
subscriptionLevelId: 'subscriptionLevelId'
groupId: 'groupId'
} as const
export type ProclamationScalarFieldEnum = (typeof ProclamationScalarFieldEnum)[keyof typeof ProclamationScalarFieldEnum]
export const ProclamationGroupScalarFieldEnum = {
id: 'id',
name: 'name',
sortOrder: 'sortOrder'
} as const
export type ProclamationGroupScalarFieldEnum = (typeof ProclamationGroupScalarFieldEnum)[keyof typeof ProclamationGroupScalarFieldEnum]
export const UserProclamationsScalarFieldEnum = {
userId: 'userId',
proclamationId: 'proclamationId',
@@ -1772,27 +1782,20 @@ export type SolfeggioFrequencyScalarFieldEnum = (typeof SolfeggioFrequencyScalar
export const ProclamationCategoryScalarFieldEnum = {
id: 'id',
name: 'name',
sortOrder: 'sortOrder',
parentId: 'parentId'
sortOrder: 'sortOrder'
} as const
export type ProclamationCategoryScalarFieldEnum = (typeof ProclamationCategoryScalarFieldEnum)[keyof typeof ProclamationCategoryScalarFieldEnum]
export const ProclamationCategorySubscriptionLevelScalarFieldEnum = {
categoryId: 'categoryId',
subscriptionLevelId: 'subscriptionLevelId'
} as const
export type ProclamationCategorySubscriptionLevelScalarFieldEnum = (typeof ProclamationCategorySubscriptionLevelScalarFieldEnum)[keyof typeof ProclamationCategorySubscriptionLevelScalarFieldEnum]
export const LicenseKeyScalarFieldEnum = {
id: 'id',
key: 'key',
active: 'active',
level: 'level',
userId: 'userId'
createdAt: 'createdAt',
lastValidatedAt: 'lastValidatedAt',
userId: 'userId',
email: 'email'
} as const
export type LicenseKeyScalarFieldEnum = (typeof LicenseKeyScalarFieldEnum)[keyof typeof LicenseKeyScalarFieldEnum]
@@ -1810,15 +1813,6 @@ export const ApiKeyScalarFieldEnum = {
export type ApiKeyScalarFieldEnum = (typeof ApiKeyScalarFieldEnum)[keyof typeof ApiKeyScalarFieldEnum]
export const SubscriptionLevelScalarFieldEnum = {
id: 'id',
name: 'name',
description: 'description'
} as const
export type SubscriptionLevelScalarFieldEnum = (typeof SubscriptionLevelScalarFieldEnum)[keyof typeof SubscriptionLevelScalarFieldEnum]
export const ColorPaletteScalarFieldEnum = {
solfeggioFrequencyId: 'solfeggioFrequencyId',
colors: 'colors'
@@ -1827,6 +1821,25 @@ export const ColorPaletteScalarFieldEnum = {
export type ColorPaletteScalarFieldEnum = (typeof ColorPaletteScalarFieldEnum)[keyof typeof ColorPaletteScalarFieldEnum]
export const AuditLogScalarFieldEnum = {
id: 'id',
createdAt: 'createdAt',
userId: 'userId',
email: 'email',
roleId: 'roleId',
roleName: 'roleName',
page: 'page',
action: 'action',
entity: 'entity',
entityId: 'entityId',
before: 'before',
after: 'after',
ip: 'ip'
} as const
export type AuditLogScalarFieldEnum = (typeof AuditLogScalarFieldEnum)[keyof typeof AuditLogScalarFieldEnum]
export const SortOrder = {
asc: 'asc',
desc: 'desc'
@@ -1842,6 +1855,14 @@ export const JsonNullValueInput = {
export type JsonNullValueInput = (typeof JsonNullValueInput)[keyof typeof JsonNullValueInput]
export const NullableJsonNullValueInput = {
DbNull: DbNull,
JsonNull: JsonNull
} as const
export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput]
export const QueryMode = {
default: 'default',
insensitive: 'insensitive'
@@ -2067,14 +2088,14 @@ export type GlobalOmitConfig = {
userRole?: Prisma.UserRoleOmit
verification?: Prisma.VerificationOmit
proclamation?: Prisma.ProclamationOmit
proclamationGroup?: Prisma.ProclamationGroupOmit
userProclamations?: Prisma.UserProclamationsOmit
solfeggioFrequency?: Prisma.SolfeggioFrequencyOmit
proclamationCategory?: Prisma.ProclamationCategoryOmit
proclamationCategorySubscriptionLevel?: Prisma.ProclamationCategorySubscriptionLevelOmit
licenseKey?: Prisma.LicenseKeyOmit
apiKey?: Prisma.ApiKeyOmit
subscriptionLevel?: Prisma.SubscriptionLevelOmit
colorPalette?: Prisma.ColorPaletteOmit
auditLog?: Prisma.AuditLogOmit
}
/* Types for Logging */
@@ -59,14 +59,14 @@ export const ModelName = {
UserRole: 'UserRole',
Verification: 'Verification',
Proclamation: 'Proclamation',
ProclamationGroup: 'ProclamationGroup',
UserProclamations: 'UserProclamations',
SolfeggioFrequency: 'SolfeggioFrequency',
ProclamationCategory: 'ProclamationCategory',
ProclamationCategorySubscriptionLevel: 'ProclamationCategorySubscriptionLevel',
LicenseKey: 'LicenseKey',
ApiKey: 'ApiKey',
SubscriptionLevel: 'SubscriptionLevel',
ColorPalette: 'ColorPalette'
ColorPalette: 'ColorPalette',
AuditLog: 'AuditLog'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
@@ -102,6 +102,7 @@ export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof User
export const ProfileScalarFieldEnum = {
id: 'id',
userId: 'userId',
email: 'email',
firstName: 'firstName',
lastName: 'lastName',
phoneNumber: 'phoneNumber',
@@ -187,12 +188,21 @@ export const ProclamationScalarFieldEnum = {
content: 'content',
solfeggioFrequencyId: 'solfeggioFrequencyId',
categoryId: 'categoryId',
subscriptionLevelId: 'subscriptionLevelId'
groupId: 'groupId'
} as const
export type ProclamationScalarFieldEnum = (typeof ProclamationScalarFieldEnum)[keyof typeof ProclamationScalarFieldEnum]
export const ProclamationGroupScalarFieldEnum = {
id: 'id',
name: 'name',
sortOrder: 'sortOrder'
} as const
export type ProclamationGroupScalarFieldEnum = (typeof ProclamationGroupScalarFieldEnum)[keyof typeof ProclamationGroupScalarFieldEnum]
export const UserProclamationsScalarFieldEnum = {
userId: 'userId',
proclamationId: 'proclamationId',
@@ -215,27 +225,20 @@ export type SolfeggioFrequencyScalarFieldEnum = (typeof SolfeggioFrequencyScalar
export const ProclamationCategoryScalarFieldEnum = {
id: 'id',
name: 'name',
sortOrder: 'sortOrder',
parentId: 'parentId'
sortOrder: 'sortOrder'
} as const
export type ProclamationCategoryScalarFieldEnum = (typeof ProclamationCategoryScalarFieldEnum)[keyof typeof ProclamationCategoryScalarFieldEnum]
export const ProclamationCategorySubscriptionLevelScalarFieldEnum = {
categoryId: 'categoryId',
subscriptionLevelId: 'subscriptionLevelId'
} as const
export type ProclamationCategorySubscriptionLevelScalarFieldEnum = (typeof ProclamationCategorySubscriptionLevelScalarFieldEnum)[keyof typeof ProclamationCategorySubscriptionLevelScalarFieldEnum]
export const LicenseKeyScalarFieldEnum = {
id: 'id',
key: 'key',
active: 'active',
level: 'level',
userId: 'userId'
createdAt: 'createdAt',
lastValidatedAt: 'lastValidatedAt',
userId: 'userId',
email: 'email'
} as const
export type LicenseKeyScalarFieldEnum = (typeof LicenseKeyScalarFieldEnum)[keyof typeof LicenseKeyScalarFieldEnum]
@@ -253,15 +256,6 @@ export const ApiKeyScalarFieldEnum = {
export type ApiKeyScalarFieldEnum = (typeof ApiKeyScalarFieldEnum)[keyof typeof ApiKeyScalarFieldEnum]
export const SubscriptionLevelScalarFieldEnum = {
id: 'id',
name: 'name',
description: 'description'
} as const
export type SubscriptionLevelScalarFieldEnum = (typeof SubscriptionLevelScalarFieldEnum)[keyof typeof SubscriptionLevelScalarFieldEnum]
export const ColorPaletteScalarFieldEnum = {
solfeggioFrequencyId: 'solfeggioFrequencyId',
colors: 'colors'
@@ -270,6 +264,25 @@ export const ColorPaletteScalarFieldEnum = {
export type ColorPaletteScalarFieldEnum = (typeof ColorPaletteScalarFieldEnum)[keyof typeof ColorPaletteScalarFieldEnum]
export const AuditLogScalarFieldEnum = {
id: 'id',
createdAt: 'createdAt',
userId: 'userId',
email: 'email',
roleId: 'roleId',
roleName: 'roleName',
page: 'page',
action: 'action',
entity: 'entity',
entityId: 'entityId',
before: 'before',
after: 'after',
ip: 'ip'
} as const
export type AuditLogScalarFieldEnum = (typeof AuditLogScalarFieldEnum)[keyof typeof AuditLogScalarFieldEnum]
export const SortOrder = {
asc: 'asc',
desc: 'desc'
@@ -285,6 +298,14 @@ export const JsonNullValueInput = {
export type JsonNullValueInput = (typeof JsonNullValueInput)[keyof typeof JsonNullValueInput]
export const NullableJsonNullValueInput = {
DbNull: DbNull,
JsonNull: JsonNull
} as const
export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput]
export const QueryMode = {
default: 'default',
insensitive: 'insensitive'
+2 -2
View File
@@ -16,12 +16,12 @@ export type * from './models/Account.ts'
export type * from './models/UserRole.ts'
export type * from './models/Verification.ts'
export type * from './models/Proclamation.ts'
export type * from './models/ProclamationGroup.ts'
export type * from './models/UserProclamations.ts'
export type * from './models/SolfeggioFrequency.ts'
export type * from './models/ProclamationCategory.ts'
export type * from './models/ProclamationCategorySubscriptionLevel.ts'
export type * from './models/LicenseKey.ts'
export type * from './models/ApiKey.ts'
export type * from './models/SubscriptionLevel.ts'
export type * from './models/ColorPalette.ts'
export type * from './models/AuditLog.ts'
export type * from './commonInputTypes.ts'
File diff suppressed because it is too large Load Diff
+149 -60
View File
@@ -28,24 +28,30 @@ export type LicenseKeyMinAggregateOutputType = {
id: string | null
key: string | null
active: boolean | null
level: string | null
createdAt: Date | null
lastValidatedAt: Date | null
userId: string | null
email: string | null
}
export type LicenseKeyMaxAggregateOutputType = {
id: string | null
key: string | null
active: boolean | null
level: string | null
createdAt: Date | null
lastValidatedAt: Date | null
userId: string | null
email: string | null
}
export type LicenseKeyCountAggregateOutputType = {
id: number
key: number
active: number
level: number
createdAt: number
lastValidatedAt: number
userId: number
email: number
_all: number
}
@@ -54,24 +60,30 @@ export type LicenseKeyMinAggregateInputType = {
id?: true
key?: true
active?: true
level?: true
createdAt?: true
lastValidatedAt?: true
userId?: true
email?: true
}
export type LicenseKeyMaxAggregateInputType = {
id?: true
key?: true
active?: true
level?: true
createdAt?: true
lastValidatedAt?: true
userId?: true
email?: true
}
export type LicenseKeyCountAggregateInputType = {
id?: true
key?: true
active?: true
level?: true
createdAt?: true
lastValidatedAt?: true
userId?: true
email?: true
_all?: true
}
@@ -151,8 +163,10 @@ export type LicenseKeyGroupByOutputType = {
id: string
key: string
active: boolean
level: string
userId: string
createdAt: Date
lastValidatedAt: Date | null
userId: string | null
email: string | null
_count: LicenseKeyCountAggregateOutputType | null
_min: LicenseKeyMinAggregateOutputType | null
_max: LicenseKeyMaxAggregateOutputType | null
@@ -180,17 +194,21 @@ export type LicenseKeyWhereInput = {
id?: Prisma.StringFilter<"LicenseKey"> | string
key?: Prisma.StringFilter<"LicenseKey"> | string
active?: Prisma.BoolFilter<"LicenseKey"> | boolean
level?: Prisma.StringFilter<"LicenseKey"> | string
userId?: Prisma.StringFilter<"LicenseKey"> | string
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
createdAt?: Prisma.DateTimeFilter<"LicenseKey"> | Date | string
lastValidatedAt?: Prisma.DateTimeNullableFilter<"LicenseKey"> | Date | string | null
userId?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
email?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
user?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
}
export type LicenseKeyOrderByWithRelationInput = {
id?: Prisma.SortOrder
key?: Prisma.SortOrder
active?: Prisma.SortOrder
level?: Prisma.SortOrder
userId?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
lastValidatedAt?: Prisma.SortOrderInput | Prisma.SortOrder
userId?: Prisma.SortOrderInput | Prisma.SortOrder
email?: Prisma.SortOrderInput | Prisma.SortOrder
user?: Prisma.UserOrderByWithRelationInput
}
@@ -201,17 +219,21 @@ export type LicenseKeyWhereUniqueInput = Prisma.AtLeast<{
OR?: Prisma.LicenseKeyWhereInput[]
NOT?: Prisma.LicenseKeyWhereInput | Prisma.LicenseKeyWhereInput[]
active?: Prisma.BoolFilter<"LicenseKey"> | boolean
level?: Prisma.StringFilter<"LicenseKey"> | string
userId?: Prisma.StringFilter<"LicenseKey"> | string
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
createdAt?: Prisma.DateTimeFilter<"LicenseKey"> | Date | string
lastValidatedAt?: Prisma.DateTimeNullableFilter<"LicenseKey"> | Date | string | null
userId?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
email?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
user?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
}, "id" | "key">
export type LicenseKeyOrderByWithAggregationInput = {
id?: Prisma.SortOrder
key?: Prisma.SortOrder
active?: Prisma.SortOrder
level?: Prisma.SortOrder
userId?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
lastValidatedAt?: Prisma.SortOrderInput | Prisma.SortOrder
userId?: Prisma.SortOrderInput | Prisma.SortOrder
email?: Prisma.SortOrderInput | Prisma.SortOrder
_count?: Prisma.LicenseKeyCountOrderByAggregateInput
_max?: Prisma.LicenseKeyMaxOrderByAggregateInput
_min?: Prisma.LicenseKeyMinOrderByAggregateInput
@@ -224,63 +246,79 @@ export type LicenseKeyScalarWhereWithAggregatesInput = {
id?: Prisma.StringWithAggregatesFilter<"LicenseKey"> | string
key?: Prisma.StringWithAggregatesFilter<"LicenseKey"> | string
active?: Prisma.BoolWithAggregatesFilter<"LicenseKey"> | boolean
level?: Prisma.StringWithAggregatesFilter<"LicenseKey"> | string
userId?: Prisma.StringWithAggregatesFilter<"LicenseKey"> | string
createdAt?: Prisma.DateTimeWithAggregatesFilter<"LicenseKey"> | Date | string
lastValidatedAt?: Prisma.DateTimeNullableWithAggregatesFilter<"LicenseKey"> | Date | string | null
userId?: Prisma.StringNullableWithAggregatesFilter<"LicenseKey"> | string | null
email?: Prisma.StringNullableWithAggregatesFilter<"LicenseKey"> | string | null
}
export type LicenseKeyCreateInput = {
id?: string
key: string
active: boolean
level: string
user: Prisma.UserCreateNestedOneWithoutLicenseKeysInput
createdAt?: Date | string
lastValidatedAt?: Date | string | null
email?: string | null
user?: Prisma.UserCreateNestedOneWithoutLicenseKeysInput
}
export type LicenseKeyUncheckedCreateInput = {
id?: string
key: string
active: boolean
level: string
userId: string
createdAt?: Date | string
lastValidatedAt?: Date | string | null
userId?: string | null
email?: string | null
}
export type LicenseKeyUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string
user?: Prisma.UserUpdateOneRequiredWithoutLicenseKeysNestedInput
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
lastValidatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
user?: Prisma.UserUpdateOneWithoutLicenseKeysNestedInput
}
export type LicenseKeyUncheckedUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string
userId?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
lastValidatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type LicenseKeyCreateManyInput = {
id?: string
key: string
active: boolean
level: string
userId: string
createdAt?: Date | string
lastValidatedAt?: Date | string | null
userId?: string | null
email?: string | null
}
export type LicenseKeyUpdateManyMutationInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
lastValidatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type LicenseKeyUncheckedUpdateManyInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string
userId?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
lastValidatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type LicenseKeyListRelationFilter = {
@@ -297,24 +335,30 @@ export type LicenseKeyCountOrderByAggregateInput = {
id?: Prisma.SortOrder
key?: Prisma.SortOrder
active?: Prisma.SortOrder
level?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
lastValidatedAt?: Prisma.SortOrder
userId?: Prisma.SortOrder
email?: Prisma.SortOrder
}
export type LicenseKeyMaxOrderByAggregateInput = {
id?: Prisma.SortOrder
key?: Prisma.SortOrder
active?: Prisma.SortOrder
level?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
lastValidatedAt?: Prisma.SortOrder
userId?: Prisma.SortOrder
email?: Prisma.SortOrder
}
export type LicenseKeyMinOrderByAggregateInput = {
id?: Prisma.SortOrder
key?: Prisma.SortOrder
active?: Prisma.SortOrder
level?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
lastValidatedAt?: Prisma.SortOrder
userId?: Prisma.SortOrder
email?: Prisma.SortOrder
}
export type LicenseKeyCreateNestedManyWithoutUserInput = {
@@ -363,14 +407,18 @@ export type LicenseKeyCreateWithoutUserInput = {
id?: string
key: string
active: boolean
level: string
createdAt?: Date | string
lastValidatedAt?: Date | string | null
email?: string | null
}
export type LicenseKeyUncheckedCreateWithoutUserInput = {
id?: string
key: string
active: boolean
level: string
createdAt?: Date | string
lastValidatedAt?: Date | string | null
email?: string | null
}
export type LicenseKeyCreateOrConnectWithoutUserInput = {
@@ -406,36 +454,46 @@ export type LicenseKeyScalarWhereInput = {
id?: Prisma.StringFilter<"LicenseKey"> | string
key?: Prisma.StringFilter<"LicenseKey"> | string
active?: Prisma.BoolFilter<"LicenseKey"> | boolean
level?: Prisma.StringFilter<"LicenseKey"> | string
userId?: Prisma.StringFilter<"LicenseKey"> | string
createdAt?: Prisma.DateTimeFilter<"LicenseKey"> | Date | string
lastValidatedAt?: Prisma.DateTimeNullableFilter<"LicenseKey"> | Date | string | null
userId?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
email?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
}
export type LicenseKeyCreateManyUserInput = {
id?: string
key: string
active: boolean
level: string
createdAt?: Date | string
lastValidatedAt?: Date | string | null
email?: string | null
}
export type LicenseKeyUpdateWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
lastValidatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type LicenseKeyUncheckedUpdateWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
lastValidatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type LicenseKeyUncheckedUpdateManyWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
lastValidatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
@@ -444,59 +502,69 @@ export type LicenseKeySelect<ExtArgs extends runtime.Types.Extensions.InternalAr
id?: boolean
key?: boolean
active?: boolean
level?: boolean
createdAt?: boolean
lastValidatedAt?: boolean
userId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
email?: boolean
user?: boolean | Prisma.LicenseKey$userArgs<ExtArgs>
}, ExtArgs["result"]["licenseKey"]>
export type LicenseKeySelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
key?: boolean
active?: boolean
level?: boolean
createdAt?: boolean
lastValidatedAt?: boolean
userId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
email?: boolean
user?: boolean | Prisma.LicenseKey$userArgs<ExtArgs>
}, ExtArgs["result"]["licenseKey"]>
export type LicenseKeySelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
key?: boolean
active?: boolean
level?: boolean
createdAt?: boolean
lastValidatedAt?: boolean
userId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
email?: boolean
user?: boolean | Prisma.LicenseKey$userArgs<ExtArgs>
}, ExtArgs["result"]["licenseKey"]>
export type LicenseKeySelectScalar = {
id?: boolean
key?: boolean
active?: boolean
level?: boolean
createdAt?: boolean
lastValidatedAt?: 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" | "createdAt" | "lastValidatedAt" | "userId" | "email", ExtArgs["result"]["licenseKey"]>
export type LicenseKeyInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
user?: boolean | Prisma.LicenseKey$userArgs<ExtArgs>
}
export type LicenseKeyIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
user?: boolean | Prisma.LicenseKey$userArgs<ExtArgs>
}
export type LicenseKeyIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
user?: boolean | Prisma.LicenseKey$userArgs<ExtArgs>
}
export type $LicenseKeyPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "LicenseKey"
objects: {
user: Prisma.$UserPayload<ExtArgs>
user: Prisma.$UserPayload<ExtArgs> | null
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string
key: string
active: boolean
level: string
userId: string
createdAt: Date
lastValidatedAt: Date | null
userId: string | null
email: string | null
}, ExtArgs["result"]["licenseKey"]>
composites: {}
}
@@ -891,7 +959,7 @@ readonly fields: LicenseKeyFieldRefs;
*/
export interface Prisma__LicenseKeyClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
readonly [Symbol.toStringTag]: "PrismaPromise"
user<T extends Prisma.UserDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.UserDefaultArgs<ExtArgs>>): Prisma.Prisma__UserClient<runtime.Types.Result.GetResult<Prisma.$UserPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
user<T extends Prisma.LicenseKey$userArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.LicenseKey$userArgs<ExtArgs>>): Prisma.Prisma__UserClient<runtime.Types.Result.GetResult<Prisma.$UserPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
@@ -924,8 +992,10 @@ export interface LicenseKeyFieldRefs {
readonly id: Prisma.FieldRef<"LicenseKey", 'String'>
readonly key: Prisma.FieldRef<"LicenseKey", 'String'>
readonly active: Prisma.FieldRef<"LicenseKey", 'Boolean'>
readonly level: Prisma.FieldRef<"LicenseKey", 'String'>
readonly createdAt: Prisma.FieldRef<"LicenseKey", 'DateTime'>
readonly lastValidatedAt: Prisma.FieldRef<"LicenseKey", 'DateTime'>
readonly userId: Prisma.FieldRef<"LicenseKey", 'String'>
readonly email: Prisma.FieldRef<"LicenseKey", 'String'>
}
@@ -1326,6 +1396,25 @@ export type LicenseKeyDeleteManyArgs<ExtArgs extends runtime.Types.Extensions.In
limit?: number
}
/**
* LicenseKey.user
*/
export type LicenseKey$userArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the User
*/
select?: Prisma.UserSelect<ExtArgs> | null
/**
* Omit specific fields from the User
*/
omit?: Prisma.UserOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.UserInclude<ExtArgs> | null
where?: Prisma.UserWhereInput
}
/**
* LicenseKey without action
*/
+177 -177
View File
@@ -29,13 +29,13 @@ export type AggregateProclamation = {
export type ProclamationAvgAggregateOutputType = {
solfeggioFrequencyId: number | null
categoryId: number | null
subscriptionLevelId: number | null
groupId: number | null
}
export type ProclamationSumAggregateOutputType = {
solfeggioFrequencyId: number | null
categoryId: number | null
subscriptionLevelId: number | null
groupId: number | null
}
export type ProclamationMinAggregateOutputType = {
@@ -44,7 +44,7 @@ export type ProclamationMinAggregateOutputType = {
content: string | null
solfeggioFrequencyId: number | null
categoryId: number | null
subscriptionLevelId: number | null
groupId: number | null
}
export type ProclamationMaxAggregateOutputType = {
@@ -53,7 +53,7 @@ export type ProclamationMaxAggregateOutputType = {
content: string | null
solfeggioFrequencyId: number | null
categoryId: number | null
subscriptionLevelId: number | null
groupId: number | null
}
export type ProclamationCountAggregateOutputType = {
@@ -62,7 +62,7 @@ export type ProclamationCountAggregateOutputType = {
content: number
solfeggioFrequencyId: number
categoryId: number
subscriptionLevelId: number
groupId: number
_all: number
}
@@ -70,13 +70,13 @@ export type ProclamationCountAggregateOutputType = {
export type ProclamationAvgAggregateInputType = {
solfeggioFrequencyId?: true
categoryId?: true
subscriptionLevelId?: true
groupId?: true
}
export type ProclamationSumAggregateInputType = {
solfeggioFrequencyId?: true
categoryId?: true
subscriptionLevelId?: true
groupId?: true
}
export type ProclamationMinAggregateInputType = {
@@ -85,7 +85,7 @@ export type ProclamationMinAggregateInputType = {
content?: true
solfeggioFrequencyId?: true
categoryId?: true
subscriptionLevelId?: true
groupId?: true
}
export type ProclamationMaxAggregateInputType = {
@@ -94,7 +94,7 @@ export type ProclamationMaxAggregateInputType = {
content?: true
solfeggioFrequencyId?: true
categoryId?: true
subscriptionLevelId?: true
groupId?: true
}
export type ProclamationCountAggregateInputType = {
@@ -103,7 +103,7 @@ export type ProclamationCountAggregateInputType = {
content?: true
solfeggioFrequencyId?: true
categoryId?: true
subscriptionLevelId?: true
groupId?: true
_all?: true
}
@@ -199,7 +199,7 @@ export type ProclamationGroupByOutputType = {
content: string
solfeggioFrequencyId: number
categoryId: number
subscriptionLevelId: number
groupId: number
_count: ProclamationCountAggregateOutputType | null
_avg: ProclamationAvgAggregateOutputType | null
_sum: ProclamationSumAggregateOutputType | null
@@ -231,11 +231,11 @@ export type ProclamationWhereInput = {
content?: Prisma.StringFilter<"Proclamation"> | string
solfeggioFrequencyId?: Prisma.IntFilter<"Proclamation"> | number
categoryId?: Prisma.IntFilter<"Proclamation"> | number
subscriptionLevelId?: Prisma.IntFilter<"Proclamation"> | number
groupId?: Prisma.IntFilter<"Proclamation"> | number
solfeggioFrequency?: Prisma.XOR<Prisma.SolfeggioFrequencyScalarRelationFilter, Prisma.SolfeggioFrequencyWhereInput>
category?: Prisma.XOR<Prisma.ProclamationCategoryScalarRelationFilter, Prisma.ProclamationCategoryWhereInput>
group?: Prisma.XOR<Prisma.ProclamationGroupScalarRelationFilter, Prisma.ProclamationGroupWhereInput>
userProclamations?: Prisma.UserProclamationsListRelationFilter
subscriptionLevel?: Prisma.XOR<Prisma.SubscriptionLevelScalarRelationFilter, Prisma.SubscriptionLevelWhereInput>
}
export type ProclamationOrderByWithRelationInput = {
@@ -244,11 +244,11 @@ export type ProclamationOrderByWithRelationInput = {
content?: Prisma.SortOrder
solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder
groupId?: Prisma.SortOrder
solfeggioFrequency?: Prisma.SolfeggioFrequencyOrderByWithRelationInput
category?: Prisma.ProclamationCategoryOrderByWithRelationInput
group?: Prisma.ProclamationGroupOrderByWithRelationInput
userProclamations?: Prisma.UserProclamationsOrderByRelationAggregateInput
subscriptionLevel?: Prisma.SubscriptionLevelOrderByWithRelationInput
}
export type ProclamationWhereUniqueInput = Prisma.AtLeast<{
@@ -260,11 +260,11 @@ export type ProclamationWhereUniqueInput = Prisma.AtLeast<{
content?: Prisma.StringFilter<"Proclamation"> | string
solfeggioFrequencyId?: Prisma.IntFilter<"Proclamation"> | number
categoryId?: Prisma.IntFilter<"Proclamation"> | number
subscriptionLevelId?: Prisma.IntFilter<"Proclamation"> | number
groupId?: Prisma.IntFilter<"Proclamation"> | number
solfeggioFrequency?: Prisma.XOR<Prisma.SolfeggioFrequencyScalarRelationFilter, Prisma.SolfeggioFrequencyWhereInput>
category?: Prisma.XOR<Prisma.ProclamationCategoryScalarRelationFilter, Prisma.ProclamationCategoryWhereInput>
group?: Prisma.XOR<Prisma.ProclamationGroupScalarRelationFilter, Prisma.ProclamationGroupWhereInput>
userProclamations?: Prisma.UserProclamationsListRelationFilter
subscriptionLevel?: Prisma.XOR<Prisma.SubscriptionLevelScalarRelationFilter, Prisma.SubscriptionLevelWhereInput>
}, "id" | "name">
export type ProclamationOrderByWithAggregationInput = {
@@ -273,7 +273,7 @@ export type ProclamationOrderByWithAggregationInput = {
content?: Prisma.SortOrder
solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder
groupId?: Prisma.SortOrder
_count?: Prisma.ProclamationCountOrderByAggregateInput
_avg?: Prisma.ProclamationAvgOrderByAggregateInput
_max?: Prisma.ProclamationMaxOrderByAggregateInput
@@ -290,7 +290,7 @@ export type ProclamationScalarWhereWithAggregatesInput = {
content?: Prisma.StringWithAggregatesFilter<"Proclamation"> | string
solfeggioFrequencyId?: Prisma.IntWithAggregatesFilter<"Proclamation"> | number
categoryId?: Prisma.IntWithAggregatesFilter<"Proclamation"> | number
subscriptionLevelId?: Prisma.IntWithAggregatesFilter<"Proclamation"> | number
groupId?: Prisma.IntWithAggregatesFilter<"Proclamation"> | number
}
export type ProclamationCreateInput = {
@@ -299,8 +299,8 @@ export type ProclamationCreateInput = {
content: string
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
group: Prisma.ProclamationGroupCreateNestedOneWithoutProclamationsInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
subscriptionLevel: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput
}
export type ProclamationUncheckedCreateInput = {
@@ -309,7 +309,7 @@ export type ProclamationUncheckedCreateInput = {
content: string
solfeggioFrequencyId: number
categoryId: number
subscriptionLevelId: number
groupId: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
}
@@ -319,8 +319,8 @@ export type ProclamationUpdateInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
group?: Prisma.ProclamationGroupUpdateOneRequiredWithoutProclamationsNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput
}
export type ProclamationUncheckedUpdateInput = {
@@ -329,7 +329,7 @@ export type ProclamationUncheckedUpdateInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
groupId?: Prisma.IntFieldUpdateOperationsInput | number
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
}
@@ -339,7 +339,7 @@ export type ProclamationCreateManyInput = {
content: string
solfeggioFrequencyId: number
categoryId: number
subscriptionLevelId: number
groupId: number
}
export type ProclamationUpdateManyMutationInput = {
@@ -354,7 +354,7 @@ export type ProclamationUncheckedUpdateManyInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
groupId?: Prisma.IntFieldUpdateOperationsInput | number
}
export type ProclamationCountOrderByAggregateInput = {
@@ -363,13 +363,13 @@ export type ProclamationCountOrderByAggregateInput = {
content?: Prisma.SortOrder
solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder
groupId?: Prisma.SortOrder
}
export type ProclamationAvgOrderByAggregateInput = {
solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder
groupId?: Prisma.SortOrder
}
export type ProclamationMaxOrderByAggregateInput = {
@@ -378,7 +378,7 @@ export type ProclamationMaxOrderByAggregateInput = {
content?: Prisma.SortOrder
solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder
groupId?: Prisma.SortOrder
}
export type ProclamationMinOrderByAggregateInput = {
@@ -387,18 +387,13 @@ export type ProclamationMinOrderByAggregateInput = {
content?: Prisma.SortOrder
solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder
groupId?: Prisma.SortOrder
}
export type ProclamationSumOrderByAggregateInput = {
solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder
}
export type ProclamationScalarRelationFilter = {
is?: Prisma.ProclamationWhereInput
isNot?: Prisma.ProclamationWhereInput
groupId?: Prisma.SortOrder
}
export type ProclamationListRelationFilter = {
@@ -411,6 +406,53 @@ export type ProclamationOrderByRelationAggregateInput = {
_count?: Prisma.SortOrder
}
export type ProclamationScalarRelationFilter = {
is?: Prisma.ProclamationWhereInput
isNot?: Prisma.ProclamationWhereInput
}
export type ProclamationCreateNestedManyWithoutGroupInput = {
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutGroupInput, Prisma.ProclamationUncheckedCreateWithoutGroupInput> | Prisma.ProclamationCreateWithoutGroupInput[] | Prisma.ProclamationUncheckedCreateWithoutGroupInput[]
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutGroupInput | Prisma.ProclamationCreateOrConnectWithoutGroupInput[]
createMany?: Prisma.ProclamationCreateManyGroupInputEnvelope
connect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
}
export type ProclamationUncheckedCreateNestedManyWithoutGroupInput = {
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutGroupInput, Prisma.ProclamationUncheckedCreateWithoutGroupInput> | Prisma.ProclamationCreateWithoutGroupInput[] | Prisma.ProclamationUncheckedCreateWithoutGroupInput[]
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutGroupInput | Prisma.ProclamationCreateOrConnectWithoutGroupInput[]
createMany?: Prisma.ProclamationCreateManyGroupInputEnvelope
connect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
}
export type ProclamationUpdateManyWithoutGroupNestedInput = {
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutGroupInput, Prisma.ProclamationUncheckedCreateWithoutGroupInput> | Prisma.ProclamationCreateWithoutGroupInput[] | Prisma.ProclamationUncheckedCreateWithoutGroupInput[]
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutGroupInput | Prisma.ProclamationCreateOrConnectWithoutGroupInput[]
upsert?: Prisma.ProclamationUpsertWithWhereUniqueWithoutGroupInput | Prisma.ProclamationUpsertWithWhereUniqueWithoutGroupInput[]
createMany?: Prisma.ProclamationCreateManyGroupInputEnvelope
set?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
disconnect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
delete?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
connect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
update?: Prisma.ProclamationUpdateWithWhereUniqueWithoutGroupInput | Prisma.ProclamationUpdateWithWhereUniqueWithoutGroupInput[]
updateMany?: Prisma.ProclamationUpdateManyWithWhereWithoutGroupInput | Prisma.ProclamationUpdateManyWithWhereWithoutGroupInput[]
deleteMany?: Prisma.ProclamationScalarWhereInput | Prisma.ProclamationScalarWhereInput[]
}
export type ProclamationUncheckedUpdateManyWithoutGroupNestedInput = {
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutGroupInput, Prisma.ProclamationUncheckedCreateWithoutGroupInput> | Prisma.ProclamationCreateWithoutGroupInput[] | Prisma.ProclamationUncheckedCreateWithoutGroupInput[]
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutGroupInput | Prisma.ProclamationCreateOrConnectWithoutGroupInput[]
upsert?: Prisma.ProclamationUpsertWithWhereUniqueWithoutGroupInput | Prisma.ProclamationUpsertWithWhereUniqueWithoutGroupInput[]
createMany?: Prisma.ProclamationCreateManyGroupInputEnvelope
set?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
disconnect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
delete?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
connect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
update?: Prisma.ProclamationUpdateWithWhereUniqueWithoutGroupInput | Prisma.ProclamationUpdateWithWhereUniqueWithoutGroupInput[]
updateMany?: Prisma.ProclamationUpdateManyWithWhereWithoutGroupInput | Prisma.ProclamationUpdateManyWithWhereWithoutGroupInput[]
deleteMany?: Prisma.ProclamationScalarWhereInput | Prisma.ProclamationScalarWhereInput[]
}
export type ProclamationCreateNestedOneWithoutUserProclamationsInput = {
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutUserProclamationsInput, Prisma.ProclamationUncheckedCreateWithoutUserProclamationsInput>
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutUserProclamationsInput
@@ -509,46 +551,60 @@ export type ProclamationUncheckedUpdateManyWithoutCategoryNestedInput = {
deleteMany?: Prisma.ProclamationScalarWhereInput | Prisma.ProclamationScalarWhereInput[]
}
export type ProclamationCreateNestedManyWithoutSubscriptionLevelInput = {
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput> | Prisma.ProclamationCreateWithoutSubscriptionLevelInput[] | Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput[]
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutSubscriptionLevelInput | Prisma.ProclamationCreateOrConnectWithoutSubscriptionLevelInput[]
createMany?: Prisma.ProclamationCreateManySubscriptionLevelInputEnvelope
connect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
export type ProclamationCreateWithoutGroupInput = {
id?: string
name: string
content: string
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
}
export type ProclamationUncheckedCreateNestedManyWithoutSubscriptionLevelInput = {
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput> | Prisma.ProclamationCreateWithoutSubscriptionLevelInput[] | Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput[]
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutSubscriptionLevelInput | Prisma.ProclamationCreateOrConnectWithoutSubscriptionLevelInput[]
createMany?: Prisma.ProclamationCreateManySubscriptionLevelInputEnvelope
connect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
export type ProclamationUncheckedCreateWithoutGroupInput = {
id?: string
name: string
content: string
solfeggioFrequencyId: number
categoryId: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
}
export type ProclamationUpdateManyWithoutSubscriptionLevelNestedInput = {
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput> | Prisma.ProclamationCreateWithoutSubscriptionLevelInput[] | Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput[]
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutSubscriptionLevelInput | Prisma.ProclamationCreateOrConnectWithoutSubscriptionLevelInput[]
upsert?: Prisma.ProclamationUpsertWithWhereUniqueWithoutSubscriptionLevelInput | Prisma.ProclamationUpsertWithWhereUniqueWithoutSubscriptionLevelInput[]
createMany?: Prisma.ProclamationCreateManySubscriptionLevelInputEnvelope
set?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
disconnect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
delete?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
connect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
update?: Prisma.ProclamationUpdateWithWhereUniqueWithoutSubscriptionLevelInput | Prisma.ProclamationUpdateWithWhereUniqueWithoutSubscriptionLevelInput[]
updateMany?: Prisma.ProclamationUpdateManyWithWhereWithoutSubscriptionLevelInput | Prisma.ProclamationUpdateManyWithWhereWithoutSubscriptionLevelInput[]
deleteMany?: Prisma.ProclamationScalarWhereInput | Prisma.ProclamationScalarWhereInput[]
export type ProclamationCreateOrConnectWithoutGroupInput = {
where: Prisma.ProclamationWhereUniqueInput
create: Prisma.XOR<Prisma.ProclamationCreateWithoutGroupInput, Prisma.ProclamationUncheckedCreateWithoutGroupInput>
}
export type ProclamationUncheckedUpdateManyWithoutSubscriptionLevelNestedInput = {
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput> | Prisma.ProclamationCreateWithoutSubscriptionLevelInput[] | Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput[]
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutSubscriptionLevelInput | Prisma.ProclamationCreateOrConnectWithoutSubscriptionLevelInput[]
upsert?: Prisma.ProclamationUpsertWithWhereUniqueWithoutSubscriptionLevelInput | Prisma.ProclamationUpsertWithWhereUniqueWithoutSubscriptionLevelInput[]
createMany?: Prisma.ProclamationCreateManySubscriptionLevelInputEnvelope
set?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
disconnect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
delete?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
connect?: Prisma.ProclamationWhereUniqueInput | Prisma.ProclamationWhereUniqueInput[]
update?: Prisma.ProclamationUpdateWithWhereUniqueWithoutSubscriptionLevelInput | Prisma.ProclamationUpdateWithWhereUniqueWithoutSubscriptionLevelInput[]
updateMany?: Prisma.ProclamationUpdateManyWithWhereWithoutSubscriptionLevelInput | Prisma.ProclamationUpdateManyWithWhereWithoutSubscriptionLevelInput[]
deleteMany?: Prisma.ProclamationScalarWhereInput | Prisma.ProclamationScalarWhereInput[]
export type ProclamationCreateManyGroupInputEnvelope = {
data: Prisma.ProclamationCreateManyGroupInput | Prisma.ProclamationCreateManyGroupInput[]
skipDuplicates?: boolean
}
export type ProclamationUpsertWithWhereUniqueWithoutGroupInput = {
where: Prisma.ProclamationWhereUniqueInput
update: Prisma.XOR<Prisma.ProclamationUpdateWithoutGroupInput, Prisma.ProclamationUncheckedUpdateWithoutGroupInput>
create: Prisma.XOR<Prisma.ProclamationCreateWithoutGroupInput, Prisma.ProclamationUncheckedCreateWithoutGroupInput>
}
export type ProclamationUpdateWithWhereUniqueWithoutGroupInput = {
where: Prisma.ProclamationWhereUniqueInput
data: Prisma.XOR<Prisma.ProclamationUpdateWithoutGroupInput, Prisma.ProclamationUncheckedUpdateWithoutGroupInput>
}
export type ProclamationUpdateManyWithWhereWithoutGroupInput = {
where: Prisma.ProclamationScalarWhereInput
data: Prisma.XOR<Prisma.ProclamationUpdateManyMutationInput, Prisma.ProclamationUncheckedUpdateManyWithoutGroupInput>
}
export type ProclamationScalarWhereInput = {
AND?: Prisma.ProclamationScalarWhereInput | Prisma.ProclamationScalarWhereInput[]
OR?: Prisma.ProclamationScalarWhereInput[]
NOT?: Prisma.ProclamationScalarWhereInput | Prisma.ProclamationScalarWhereInput[]
id?: Prisma.StringFilter<"Proclamation"> | string
name?: Prisma.StringFilter<"Proclamation"> | string
content?: Prisma.StringFilter<"Proclamation"> | string
solfeggioFrequencyId?: Prisma.IntFilter<"Proclamation"> | number
categoryId?: Prisma.IntFilter<"Proclamation"> | number
groupId?: Prisma.IntFilter<"Proclamation"> | number
}
export type ProclamationCreateWithoutUserProclamationsInput = {
@@ -557,7 +613,7 @@ export type ProclamationCreateWithoutUserProclamationsInput = {
content: string
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
subscriptionLevel: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput
group: Prisma.ProclamationGroupCreateNestedOneWithoutProclamationsInput
}
export type ProclamationUncheckedCreateWithoutUserProclamationsInput = {
@@ -566,7 +622,7 @@ export type ProclamationUncheckedCreateWithoutUserProclamationsInput = {
content: string
solfeggioFrequencyId: number
categoryId: number
subscriptionLevelId: number
groupId: number
}
export type ProclamationCreateOrConnectWithoutUserProclamationsInput = {
@@ -591,7 +647,7 @@ export type ProclamationUpdateWithoutUserProclamationsInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput
group?: Prisma.ProclamationGroupUpdateOneRequiredWithoutProclamationsNestedInput
}
export type ProclamationUncheckedUpdateWithoutUserProclamationsInput = {
@@ -600,7 +656,7 @@ export type ProclamationUncheckedUpdateWithoutUserProclamationsInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
groupId?: Prisma.IntFieldUpdateOperationsInput | number
}
export type ProclamationCreateWithoutSolfeggioFrequencyInput = {
@@ -608,8 +664,8 @@ export type ProclamationCreateWithoutSolfeggioFrequencyInput = {
name: string
content: string
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
group: Prisma.ProclamationGroupCreateNestedOneWithoutProclamationsInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
subscriptionLevel: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput
}
export type ProclamationUncheckedCreateWithoutSolfeggioFrequencyInput = {
@@ -617,7 +673,7 @@ export type ProclamationUncheckedCreateWithoutSolfeggioFrequencyInput = {
name: string
content: string
categoryId: number
subscriptionLevelId: number
groupId: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
}
@@ -647,25 +703,13 @@ export type ProclamationUpdateManyWithWhereWithoutSolfeggioFrequencyInput = {
data: Prisma.XOR<Prisma.ProclamationUpdateManyMutationInput, Prisma.ProclamationUncheckedUpdateManyWithoutSolfeggioFrequencyInput>
}
export type ProclamationScalarWhereInput = {
AND?: Prisma.ProclamationScalarWhereInput | Prisma.ProclamationScalarWhereInput[]
OR?: Prisma.ProclamationScalarWhereInput[]
NOT?: Prisma.ProclamationScalarWhereInput | Prisma.ProclamationScalarWhereInput[]
id?: Prisma.StringFilter<"Proclamation"> | string
name?: Prisma.StringFilter<"Proclamation"> | string
content?: Prisma.StringFilter<"Proclamation"> | string
solfeggioFrequencyId?: Prisma.IntFilter<"Proclamation"> | number
categoryId?: Prisma.IntFilter<"Proclamation"> | number
subscriptionLevelId?: Prisma.IntFilter<"Proclamation"> | number
}
export type ProclamationCreateWithoutCategoryInput = {
id?: string
name: string
content: string
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
group: Prisma.ProclamationGroupCreateNestedOneWithoutProclamationsInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
subscriptionLevel: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput
}
export type ProclamationUncheckedCreateWithoutCategoryInput = {
@@ -673,7 +717,7 @@ export type ProclamationUncheckedCreateWithoutCategoryInput = {
name: string
content: string
solfeggioFrequencyId: number
subscriptionLevelId: number
groupId: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
}
@@ -703,48 +747,38 @@ export type ProclamationUpdateManyWithWhereWithoutCategoryInput = {
data: Prisma.XOR<Prisma.ProclamationUpdateManyMutationInput, Prisma.ProclamationUncheckedUpdateManyWithoutCategoryInput>
}
export type ProclamationCreateWithoutSubscriptionLevelInput = {
id?: string
name: string
content: string
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
}
export type ProclamationUncheckedCreateWithoutSubscriptionLevelInput = {
export type ProclamationCreateManyGroupInput = {
id?: string
name: string
content: string
solfeggioFrequencyId: number
categoryId: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
}
export type ProclamationCreateOrConnectWithoutSubscriptionLevelInput = {
where: Prisma.ProclamationWhereUniqueInput
create: Prisma.XOR<Prisma.ProclamationCreateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput>
export type ProclamationUpdateWithoutGroupInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
}
export type ProclamationCreateManySubscriptionLevelInputEnvelope = {
data: Prisma.ProclamationCreateManySubscriptionLevelInput | Prisma.ProclamationCreateManySubscriptionLevelInput[]
skipDuplicates?: boolean
export type ProclamationUncheckedUpdateWithoutGroupInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
}
export type ProclamationUpsertWithWhereUniqueWithoutSubscriptionLevelInput = {
where: Prisma.ProclamationWhereUniqueInput
update: Prisma.XOR<Prisma.ProclamationUpdateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedUpdateWithoutSubscriptionLevelInput>
create: Prisma.XOR<Prisma.ProclamationCreateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput>
}
export type ProclamationUpdateWithWhereUniqueWithoutSubscriptionLevelInput = {
where: Prisma.ProclamationWhereUniqueInput
data: Prisma.XOR<Prisma.ProclamationUpdateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedUpdateWithoutSubscriptionLevelInput>
}
export type ProclamationUpdateManyWithWhereWithoutSubscriptionLevelInput = {
where: Prisma.ProclamationScalarWhereInput
data: Prisma.XOR<Prisma.ProclamationUpdateManyMutationInput, Prisma.ProclamationUncheckedUpdateManyWithoutSubscriptionLevelInput>
export type ProclamationUncheckedUpdateManyWithoutGroupInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
}
export type ProclamationCreateManySolfeggioFrequencyInput = {
@@ -752,7 +786,7 @@ export type ProclamationCreateManySolfeggioFrequencyInput = {
name: string
content: string
categoryId: number
subscriptionLevelId: number
groupId: number
}
export type ProclamationUpdateWithoutSolfeggioFrequencyInput = {
@@ -760,8 +794,8 @@ export type ProclamationUpdateWithoutSolfeggioFrequencyInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
group?: Prisma.ProclamationGroupUpdateOneRequiredWithoutProclamationsNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput
}
export type ProclamationUncheckedUpdateWithoutSolfeggioFrequencyInput = {
@@ -769,7 +803,7 @@ export type ProclamationUncheckedUpdateWithoutSolfeggioFrequencyInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
groupId?: Prisma.IntFieldUpdateOperationsInput | number
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
}
@@ -778,7 +812,7 @@ export type ProclamationUncheckedUpdateManyWithoutSolfeggioFrequencyInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
groupId?: Prisma.IntFieldUpdateOperationsInput | number
}
export type ProclamationCreateManyCategoryInput = {
@@ -786,7 +820,7 @@ export type ProclamationCreateManyCategoryInput = {
name: string
content: string
solfeggioFrequencyId: number
subscriptionLevelId: number
groupId: number
}
export type ProclamationUpdateWithoutCategoryInput = {
@@ -794,8 +828,8 @@ export type ProclamationUpdateWithoutCategoryInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
group?: Prisma.ProclamationGroupUpdateOneRequiredWithoutProclamationsNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput
}
export type ProclamationUncheckedUpdateWithoutCategoryInput = {
@@ -803,7 +837,7 @@ export type ProclamationUncheckedUpdateWithoutCategoryInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
groupId?: Prisma.IntFieldUpdateOperationsInput | number
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
}
@@ -812,41 +846,7 @@ export type ProclamationUncheckedUpdateManyWithoutCategoryInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
}
export type ProclamationCreateManySubscriptionLevelInput = {
id?: string
name: string
content: string
solfeggioFrequencyId: number
categoryId: number
}
export type ProclamationUpdateWithoutSubscriptionLevelInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
}
export type ProclamationUncheckedUpdateWithoutSubscriptionLevelInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
}
export type ProclamationUncheckedUpdateManyWithoutSubscriptionLevelInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
groupId?: Prisma.IntFieldUpdateOperationsInput | number
}
@@ -886,11 +886,11 @@ export type ProclamationSelect<ExtArgs extends runtime.Types.Extensions.Internal
content?: boolean
solfeggioFrequencyId?: boolean
categoryId?: boolean
subscriptionLevelId?: boolean
groupId?: boolean
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
group?: boolean | Prisma.ProclamationGroupDefaultArgs<ExtArgs>
userProclamations?: boolean | Prisma.Proclamation$userProclamationsArgs<ExtArgs>
subscriptionLevel?: boolean | Prisma.SubscriptionLevelDefaultArgs<ExtArgs>
_count?: boolean | Prisma.ProclamationCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["proclamation"]>
@@ -900,10 +900,10 @@ export type ProclamationSelectCreateManyAndReturn<ExtArgs extends runtime.Types.
content?: boolean
solfeggioFrequencyId?: boolean
categoryId?: boolean
subscriptionLevelId?: boolean
groupId?: boolean
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
subscriptionLevel?: boolean | Prisma.SubscriptionLevelDefaultArgs<ExtArgs>
group?: boolean | Prisma.ProclamationGroupDefaultArgs<ExtArgs>
}, ExtArgs["result"]["proclamation"]>
export type ProclamationSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
@@ -912,10 +912,10 @@ export type ProclamationSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.
content?: boolean
solfeggioFrequencyId?: boolean
categoryId?: boolean
subscriptionLevelId?: boolean
groupId?: boolean
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
subscriptionLevel?: boolean | Prisma.SubscriptionLevelDefaultArgs<ExtArgs>
group?: boolean | Prisma.ProclamationGroupDefaultArgs<ExtArgs>
}, ExtArgs["result"]["proclamation"]>
export type ProclamationSelectScalar = {
@@ -924,26 +924,26 @@ export type ProclamationSelectScalar = {
content?: boolean
solfeggioFrequencyId?: boolean
categoryId?: boolean
subscriptionLevelId?: boolean
groupId?: boolean
}
export type ProclamationOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "content" | "solfeggioFrequencyId" | "categoryId" | "subscriptionLevelId", ExtArgs["result"]["proclamation"]>
export type ProclamationOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "content" | "solfeggioFrequencyId" | "categoryId" | "groupId", ExtArgs["result"]["proclamation"]>
export type ProclamationInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
group?: boolean | Prisma.ProclamationGroupDefaultArgs<ExtArgs>
userProclamations?: boolean | Prisma.Proclamation$userProclamationsArgs<ExtArgs>
subscriptionLevel?: boolean | Prisma.SubscriptionLevelDefaultArgs<ExtArgs>
_count?: boolean | Prisma.ProclamationCountOutputTypeDefaultArgs<ExtArgs>
}
export type ProclamationIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
subscriptionLevel?: boolean | Prisma.SubscriptionLevelDefaultArgs<ExtArgs>
group?: boolean | Prisma.ProclamationGroupDefaultArgs<ExtArgs>
}
export type ProclamationIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
subscriptionLevel?: boolean | Prisma.SubscriptionLevelDefaultArgs<ExtArgs>
group?: boolean | Prisma.ProclamationGroupDefaultArgs<ExtArgs>
}
export type $ProclamationPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
@@ -951,8 +951,8 @@ export type $ProclamationPayload<ExtArgs extends runtime.Types.Extensions.Intern
objects: {
solfeggioFrequency: Prisma.$SolfeggioFrequencyPayload<ExtArgs>
category: Prisma.$ProclamationCategoryPayload<ExtArgs>
group: Prisma.$ProclamationGroupPayload<ExtArgs>
userProclamations: Prisma.$UserProclamationsPayload<ExtArgs>[]
subscriptionLevel: Prisma.$SubscriptionLevelPayload<ExtArgs>
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string
@@ -960,7 +960,7 @@ export type $ProclamationPayload<ExtArgs extends runtime.Types.Extensions.Intern
content: string
solfeggioFrequencyId: number
categoryId: number
subscriptionLevelId: number
groupId: number
}, ExtArgs["result"]["proclamation"]>
composites: {}
}
@@ -1357,8 +1357,8 @@ export interface Prisma__ProclamationClient<T, Null = never, ExtArgs extends run
readonly [Symbol.toStringTag]: "PrismaPromise"
solfeggioFrequency<T extends Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>>): Prisma.Prisma__SolfeggioFrequencyClient<runtime.Types.Result.GetResult<Prisma.$SolfeggioFrequencyPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
category<T extends Prisma.ProclamationCategoryDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.ProclamationCategoryDefaultArgs<ExtArgs>>): Prisma.Prisma__ProclamationCategoryClient<runtime.Types.Result.GetResult<Prisma.$ProclamationCategoryPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
group<T extends Prisma.ProclamationGroupDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.ProclamationGroupDefaultArgs<ExtArgs>>): Prisma.Prisma__ProclamationGroupClient<runtime.Types.Result.GetResult<Prisma.$ProclamationGroupPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
userProclamations<T extends Prisma.Proclamation$userProclamationsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Proclamation$userProclamationsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$UserProclamationsPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
subscriptionLevel<T extends Prisma.SubscriptionLevelDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.SubscriptionLevelDefaultArgs<ExtArgs>>): Prisma.Prisma__SubscriptionLevelClient<runtime.Types.Result.GetResult<Prisma.$SubscriptionLevelPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
@@ -1393,7 +1393,7 @@ export interface ProclamationFieldRefs {
readonly content: Prisma.FieldRef<"Proclamation", 'String'>
readonly solfeggioFrequencyId: Prisma.FieldRef<"Proclamation", 'Int'>
readonly categoryId: Prisma.FieldRef<"Proclamation", 'Int'>
readonly subscriptionLevelId: Prisma.FieldRef<"Proclamation", 'Int'>
readonly groupId: Prisma.FieldRef<"Proclamation", 'Int'>
}
@@ -29,34 +29,29 @@ export type AggregateProclamationCategory = {
export type ProclamationCategoryAvgAggregateOutputType = {
id: number | null
sortOrder: number | null
parentId: number | null
}
export type ProclamationCategorySumAggregateOutputType = {
id: number | null
sortOrder: number | null
parentId: number | null
}
export type ProclamationCategoryMinAggregateOutputType = {
id: number | null
name: string | null
sortOrder: number | null
parentId: number | null
}
export type ProclamationCategoryMaxAggregateOutputType = {
id: number | null
name: string | null
sortOrder: number | null
parentId: number | null
}
export type ProclamationCategoryCountAggregateOutputType = {
id: number
name: number
sortOrder: number
parentId: number
_all: number
}
@@ -64,34 +59,29 @@ export type ProclamationCategoryCountAggregateOutputType = {
export type ProclamationCategoryAvgAggregateInputType = {
id?: true
sortOrder?: true
parentId?: true
}
export type ProclamationCategorySumAggregateInputType = {
id?: true
sortOrder?: true
parentId?: true
}
export type ProclamationCategoryMinAggregateInputType = {
id?: true
name?: true
sortOrder?: true
parentId?: true
}
export type ProclamationCategoryMaxAggregateInputType = {
id?: true
name?: true
sortOrder?: true
parentId?: true
}
export type ProclamationCategoryCountAggregateInputType = {
id?: true
name?: true
sortOrder?: true
parentId?: true
_all?: true
}
@@ -185,7 +175,6 @@ export type ProclamationCategoryGroupByOutputType = {
id: number
name: string
sortOrder: number
parentId: number | null
_count: ProclamationCategoryCountAggregateOutputType | null
_avg: ProclamationCategoryAvgAggregateOutputType | null
_sum: ProclamationCategorySumAggregateOutputType | null
@@ -215,44 +204,30 @@ export type ProclamationCategoryWhereInput = {
id?: Prisma.IntFilter<"ProclamationCategory"> | number
name?: Prisma.StringFilter<"ProclamationCategory"> | string
sortOrder?: Prisma.IntFilter<"ProclamationCategory"> | number
parentId?: Prisma.IntNullableFilter<"ProclamationCategory"> | number | null
parent?: Prisma.XOR<Prisma.ProclamationCategoryNullableScalarRelationFilter, Prisma.ProclamationCategoryWhereInput> | null
children?: Prisma.ProclamationCategoryListRelationFilter
proclamations?: Prisma.ProclamationListRelationFilter
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelListRelationFilter
}
export type ProclamationCategoryOrderByWithRelationInput = {
id?: Prisma.SortOrder
name?: Prisma.SortOrder
sortOrder?: Prisma.SortOrder
parentId?: Prisma.SortOrderInput | Prisma.SortOrder
parent?: Prisma.ProclamationCategoryOrderByWithRelationInput
children?: Prisma.ProclamationCategoryOrderByRelationAggregateInput
proclamations?: Prisma.ProclamationOrderByRelationAggregateInput
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelOrderByRelationAggregateInput
}
export type ProclamationCategoryWhereUniqueInput = Prisma.AtLeast<{
id?: number
parentId_name?: Prisma.ProclamationCategoryParentIdNameCompoundUniqueInput
name?: string
AND?: Prisma.ProclamationCategoryWhereInput | Prisma.ProclamationCategoryWhereInput[]
OR?: Prisma.ProclamationCategoryWhereInput[]
NOT?: Prisma.ProclamationCategoryWhereInput | Prisma.ProclamationCategoryWhereInput[]
name?: Prisma.StringFilter<"ProclamationCategory"> | string
sortOrder?: Prisma.IntFilter<"ProclamationCategory"> | number
parentId?: Prisma.IntNullableFilter<"ProclamationCategory"> | number | null
parent?: Prisma.XOR<Prisma.ProclamationCategoryNullableScalarRelationFilter, Prisma.ProclamationCategoryWhereInput> | null
children?: Prisma.ProclamationCategoryListRelationFilter
proclamations?: Prisma.ProclamationListRelationFilter
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelListRelationFilter
}, "id" | "parentId_name">
}, "id" | "name">
export type ProclamationCategoryOrderByWithAggregationInput = {
id?: Prisma.SortOrder
name?: Prisma.SortOrder
sortOrder?: Prisma.SortOrder
parentId?: Prisma.SortOrderInput | Prisma.SortOrder
_count?: Prisma.ProclamationCategoryCountOrderByAggregateInput
_avg?: Prisma.ProclamationCategoryAvgOrderByAggregateInput
_max?: Prisma.ProclamationCategoryMaxOrderByAggregateInput
@@ -267,52 +242,38 @@ export type ProclamationCategoryScalarWhereWithAggregatesInput = {
id?: Prisma.IntWithAggregatesFilter<"ProclamationCategory"> | number
name?: Prisma.StringWithAggregatesFilter<"ProclamationCategory"> | string
sortOrder?: Prisma.IntWithAggregatesFilter<"ProclamationCategory"> | number
parentId?: Prisma.IntNullableWithAggregatesFilter<"ProclamationCategory"> | number | null
}
export type ProclamationCategoryCreateInput = {
name: string
sortOrder?: number
parent?: Prisma.ProclamationCategoryCreateNestedOneWithoutChildrenInput
children?: Prisma.ProclamationCategoryCreateNestedManyWithoutParentInput
proclamations?: Prisma.ProclamationCreateNestedManyWithoutCategoryInput
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelCreateNestedManyWithoutCategoryInput
}
export type ProclamationCategoryUncheckedCreateInput = {
id?: number
name: string
sortOrder?: number
parentId?: number | null
children?: Prisma.ProclamationCategoryUncheckedCreateNestedManyWithoutParentInput
proclamations?: Prisma.ProclamationUncheckedCreateNestedManyWithoutCategoryInput
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUncheckedCreateNestedManyWithoutCategoryInput
}
export type ProclamationCategoryUpdateInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
parent?: Prisma.ProclamationCategoryUpdateOneWithoutChildrenNestedInput
children?: Prisma.ProclamationCategoryUpdateManyWithoutParentNestedInput
proclamations?: Prisma.ProclamationUpdateManyWithoutCategoryNestedInput
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUpdateManyWithoutCategoryNestedInput
}
export type ProclamationCategoryUncheckedUpdateInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number
name?: Prisma.StringFieldUpdateOperationsInput | string
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
parentId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
children?: Prisma.ProclamationCategoryUncheckedUpdateManyWithoutParentNestedInput
proclamations?: Prisma.ProclamationUncheckedUpdateManyWithoutCategoryNestedInput
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUncheckedUpdateManyWithoutCategoryNestedInput
}
export type ProclamationCategoryCreateManyInput = {
id?: number
name: string
sortOrder?: number
parentId?: number | null
}
export type ProclamationCategoryUpdateManyMutationInput = {
@@ -324,7 +285,6 @@ export type ProclamationCategoryUncheckedUpdateManyInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number
name?: Prisma.StringFieldUpdateOperationsInput | string
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
parentId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
}
export type ProclamationCategoryScalarRelationFilter = {
@@ -332,57 +292,32 @@ export type ProclamationCategoryScalarRelationFilter = {
isNot?: Prisma.ProclamationCategoryWhereInput
}
export type ProclamationCategoryNullableScalarRelationFilter = {
is?: Prisma.ProclamationCategoryWhereInput | null
isNot?: Prisma.ProclamationCategoryWhereInput | null
}
export type ProclamationCategoryListRelationFilter = {
every?: Prisma.ProclamationCategoryWhereInput
some?: Prisma.ProclamationCategoryWhereInput
none?: Prisma.ProclamationCategoryWhereInput
}
export type ProclamationCategoryOrderByRelationAggregateInput = {
_count?: Prisma.SortOrder
}
export type ProclamationCategoryParentIdNameCompoundUniqueInput = {
parentId: number
name: string
}
export type ProclamationCategoryCountOrderByAggregateInput = {
id?: Prisma.SortOrder
name?: Prisma.SortOrder
sortOrder?: Prisma.SortOrder
parentId?: Prisma.SortOrder
}
export type ProclamationCategoryAvgOrderByAggregateInput = {
id?: Prisma.SortOrder
sortOrder?: Prisma.SortOrder
parentId?: Prisma.SortOrder
}
export type ProclamationCategoryMaxOrderByAggregateInput = {
id?: Prisma.SortOrder
name?: Prisma.SortOrder
sortOrder?: Prisma.SortOrder
parentId?: Prisma.SortOrder
}
export type ProclamationCategoryMinOrderByAggregateInput = {
id?: Prisma.SortOrder
name?: Prisma.SortOrder
sortOrder?: Prisma.SortOrder
parentId?: Prisma.SortOrder
}
export type ProclamationCategorySumOrderByAggregateInput = {
id?: Prisma.SortOrder
sortOrder?: Prisma.SortOrder
parentId?: Prisma.SortOrder
}
export type ProclamationCategoryCreateNestedOneWithoutProclamationsInput = {
@@ -399,93 +334,15 @@ export type ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
update?: Prisma.XOR<Prisma.XOR<Prisma.ProclamationCategoryUpdateToOneWithWhereWithoutProclamationsInput, Prisma.ProclamationCategoryUpdateWithoutProclamationsInput>, Prisma.ProclamationCategoryUncheckedUpdateWithoutProclamationsInput>
}
export type ProclamationCategoryCreateNestedOneWithoutChildrenInput = {
create?: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutChildrenInput, Prisma.ProclamationCategoryUncheckedCreateWithoutChildrenInput>
connectOrCreate?: Prisma.ProclamationCategoryCreateOrConnectWithoutChildrenInput
connect?: Prisma.ProclamationCategoryWhereUniqueInput
}
export type ProclamationCategoryCreateNestedManyWithoutParentInput = {
create?: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutParentInput, Prisma.ProclamationCategoryUncheckedCreateWithoutParentInput> | Prisma.ProclamationCategoryCreateWithoutParentInput[] | Prisma.ProclamationCategoryUncheckedCreateWithoutParentInput[]
connectOrCreate?: Prisma.ProclamationCategoryCreateOrConnectWithoutParentInput | Prisma.ProclamationCategoryCreateOrConnectWithoutParentInput[]
createMany?: Prisma.ProclamationCategoryCreateManyParentInputEnvelope
connect?: Prisma.ProclamationCategoryWhereUniqueInput | Prisma.ProclamationCategoryWhereUniqueInput[]
}
export type ProclamationCategoryUncheckedCreateNestedManyWithoutParentInput = {
create?: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutParentInput, Prisma.ProclamationCategoryUncheckedCreateWithoutParentInput> | Prisma.ProclamationCategoryCreateWithoutParentInput[] | Prisma.ProclamationCategoryUncheckedCreateWithoutParentInput[]
connectOrCreate?: Prisma.ProclamationCategoryCreateOrConnectWithoutParentInput | Prisma.ProclamationCategoryCreateOrConnectWithoutParentInput[]
createMany?: Prisma.ProclamationCategoryCreateManyParentInputEnvelope
connect?: Prisma.ProclamationCategoryWhereUniqueInput | Prisma.ProclamationCategoryWhereUniqueInput[]
}
export type ProclamationCategoryUpdateOneWithoutChildrenNestedInput = {
create?: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutChildrenInput, Prisma.ProclamationCategoryUncheckedCreateWithoutChildrenInput>
connectOrCreate?: Prisma.ProclamationCategoryCreateOrConnectWithoutChildrenInput
upsert?: Prisma.ProclamationCategoryUpsertWithoutChildrenInput
disconnect?: Prisma.ProclamationCategoryWhereInput | boolean
delete?: Prisma.ProclamationCategoryWhereInput | boolean
connect?: Prisma.ProclamationCategoryWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.ProclamationCategoryUpdateToOneWithWhereWithoutChildrenInput, Prisma.ProclamationCategoryUpdateWithoutChildrenInput>, Prisma.ProclamationCategoryUncheckedUpdateWithoutChildrenInput>
}
export type ProclamationCategoryUpdateManyWithoutParentNestedInput = {
create?: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutParentInput, Prisma.ProclamationCategoryUncheckedCreateWithoutParentInput> | Prisma.ProclamationCategoryCreateWithoutParentInput[] | Prisma.ProclamationCategoryUncheckedCreateWithoutParentInput[]
connectOrCreate?: Prisma.ProclamationCategoryCreateOrConnectWithoutParentInput | Prisma.ProclamationCategoryCreateOrConnectWithoutParentInput[]
upsert?: Prisma.ProclamationCategoryUpsertWithWhereUniqueWithoutParentInput | Prisma.ProclamationCategoryUpsertWithWhereUniqueWithoutParentInput[]
createMany?: Prisma.ProclamationCategoryCreateManyParentInputEnvelope
set?: Prisma.ProclamationCategoryWhereUniqueInput | Prisma.ProclamationCategoryWhereUniqueInput[]
disconnect?: Prisma.ProclamationCategoryWhereUniqueInput | Prisma.ProclamationCategoryWhereUniqueInput[]
delete?: Prisma.ProclamationCategoryWhereUniqueInput | Prisma.ProclamationCategoryWhereUniqueInput[]
connect?: Prisma.ProclamationCategoryWhereUniqueInput | Prisma.ProclamationCategoryWhereUniqueInput[]
update?: Prisma.ProclamationCategoryUpdateWithWhereUniqueWithoutParentInput | Prisma.ProclamationCategoryUpdateWithWhereUniqueWithoutParentInput[]
updateMany?: Prisma.ProclamationCategoryUpdateManyWithWhereWithoutParentInput | Prisma.ProclamationCategoryUpdateManyWithWhereWithoutParentInput[]
deleteMany?: Prisma.ProclamationCategoryScalarWhereInput | Prisma.ProclamationCategoryScalarWhereInput[]
}
export type ProclamationCategoryUncheckedUpdateManyWithoutParentNestedInput = {
create?: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutParentInput, Prisma.ProclamationCategoryUncheckedCreateWithoutParentInput> | Prisma.ProclamationCategoryCreateWithoutParentInput[] | Prisma.ProclamationCategoryUncheckedCreateWithoutParentInput[]
connectOrCreate?: Prisma.ProclamationCategoryCreateOrConnectWithoutParentInput | Prisma.ProclamationCategoryCreateOrConnectWithoutParentInput[]
upsert?: Prisma.ProclamationCategoryUpsertWithWhereUniqueWithoutParentInput | Prisma.ProclamationCategoryUpsertWithWhereUniqueWithoutParentInput[]
createMany?: Prisma.ProclamationCategoryCreateManyParentInputEnvelope
set?: Prisma.ProclamationCategoryWhereUniqueInput | Prisma.ProclamationCategoryWhereUniqueInput[]
disconnect?: Prisma.ProclamationCategoryWhereUniqueInput | Prisma.ProclamationCategoryWhereUniqueInput[]
delete?: Prisma.ProclamationCategoryWhereUniqueInput | Prisma.ProclamationCategoryWhereUniqueInput[]
connect?: Prisma.ProclamationCategoryWhereUniqueInput | Prisma.ProclamationCategoryWhereUniqueInput[]
update?: Prisma.ProclamationCategoryUpdateWithWhereUniqueWithoutParentInput | Prisma.ProclamationCategoryUpdateWithWhereUniqueWithoutParentInput[]
updateMany?: Prisma.ProclamationCategoryUpdateManyWithWhereWithoutParentInput | Prisma.ProclamationCategoryUpdateManyWithWhereWithoutParentInput[]
deleteMany?: Prisma.ProclamationCategoryScalarWhereInput | Prisma.ProclamationCategoryScalarWhereInput[]
}
export type ProclamationCategoryCreateNestedOneWithoutSubscriptionLevelsInput = {
create?: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutSubscriptionLevelsInput, Prisma.ProclamationCategoryUncheckedCreateWithoutSubscriptionLevelsInput>
connectOrCreate?: Prisma.ProclamationCategoryCreateOrConnectWithoutSubscriptionLevelsInput
connect?: Prisma.ProclamationCategoryWhereUniqueInput
}
export type ProclamationCategoryUpdateOneRequiredWithoutSubscriptionLevelsNestedInput = {
create?: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutSubscriptionLevelsInput, Prisma.ProclamationCategoryUncheckedCreateWithoutSubscriptionLevelsInput>
connectOrCreate?: Prisma.ProclamationCategoryCreateOrConnectWithoutSubscriptionLevelsInput
upsert?: Prisma.ProclamationCategoryUpsertWithoutSubscriptionLevelsInput
connect?: Prisma.ProclamationCategoryWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.ProclamationCategoryUpdateToOneWithWhereWithoutSubscriptionLevelsInput, Prisma.ProclamationCategoryUpdateWithoutSubscriptionLevelsInput>, Prisma.ProclamationCategoryUncheckedUpdateWithoutSubscriptionLevelsInput>
}
export type ProclamationCategoryCreateWithoutProclamationsInput = {
name: string
sortOrder?: number
parent?: Prisma.ProclamationCategoryCreateNestedOneWithoutChildrenInput
children?: Prisma.ProclamationCategoryCreateNestedManyWithoutParentInput
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelCreateNestedManyWithoutCategoryInput
}
export type ProclamationCategoryUncheckedCreateWithoutProclamationsInput = {
id?: number
name: string
sortOrder?: number
parentId?: number | null
children?: Prisma.ProclamationCategoryUncheckedCreateNestedManyWithoutParentInput
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUncheckedCreateNestedManyWithoutCategoryInput
}
export type ProclamationCategoryCreateOrConnectWithoutProclamationsInput = {
@@ -507,200 +364,12 @@ export type ProclamationCategoryUpdateToOneWithWhereWithoutProclamationsInput =
export type ProclamationCategoryUpdateWithoutProclamationsInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
parent?: Prisma.ProclamationCategoryUpdateOneWithoutChildrenNestedInput
children?: Prisma.ProclamationCategoryUpdateManyWithoutParentNestedInput
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUpdateManyWithoutCategoryNestedInput
}
export type ProclamationCategoryUncheckedUpdateWithoutProclamationsInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number
name?: Prisma.StringFieldUpdateOperationsInput | string
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
parentId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
children?: Prisma.ProclamationCategoryUncheckedUpdateManyWithoutParentNestedInput
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUncheckedUpdateManyWithoutCategoryNestedInput
}
export type ProclamationCategoryCreateWithoutChildrenInput = {
name: string
sortOrder?: number
parent?: Prisma.ProclamationCategoryCreateNestedOneWithoutChildrenInput
proclamations?: Prisma.ProclamationCreateNestedManyWithoutCategoryInput
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelCreateNestedManyWithoutCategoryInput
}
export type ProclamationCategoryUncheckedCreateWithoutChildrenInput = {
id?: number
name: string
sortOrder?: number
parentId?: number | null
proclamations?: Prisma.ProclamationUncheckedCreateNestedManyWithoutCategoryInput
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUncheckedCreateNestedManyWithoutCategoryInput
}
export type ProclamationCategoryCreateOrConnectWithoutChildrenInput = {
where: Prisma.ProclamationCategoryWhereUniqueInput
create: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutChildrenInput, Prisma.ProclamationCategoryUncheckedCreateWithoutChildrenInput>
}
export type ProclamationCategoryCreateWithoutParentInput = {
name: string
sortOrder?: number
children?: Prisma.ProclamationCategoryCreateNestedManyWithoutParentInput
proclamations?: Prisma.ProclamationCreateNestedManyWithoutCategoryInput
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelCreateNestedManyWithoutCategoryInput
}
export type ProclamationCategoryUncheckedCreateWithoutParentInput = {
id?: number
name: string
sortOrder?: number
children?: Prisma.ProclamationCategoryUncheckedCreateNestedManyWithoutParentInput
proclamations?: Prisma.ProclamationUncheckedCreateNestedManyWithoutCategoryInput
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUncheckedCreateNestedManyWithoutCategoryInput
}
export type ProclamationCategoryCreateOrConnectWithoutParentInput = {
where: Prisma.ProclamationCategoryWhereUniqueInput
create: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutParentInput, Prisma.ProclamationCategoryUncheckedCreateWithoutParentInput>
}
export type ProclamationCategoryCreateManyParentInputEnvelope = {
data: Prisma.ProclamationCategoryCreateManyParentInput | Prisma.ProclamationCategoryCreateManyParentInput[]
skipDuplicates?: boolean
}
export type ProclamationCategoryUpsertWithoutChildrenInput = {
update: Prisma.XOR<Prisma.ProclamationCategoryUpdateWithoutChildrenInput, Prisma.ProclamationCategoryUncheckedUpdateWithoutChildrenInput>
create: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutChildrenInput, Prisma.ProclamationCategoryUncheckedCreateWithoutChildrenInput>
where?: Prisma.ProclamationCategoryWhereInput
}
export type ProclamationCategoryUpdateToOneWithWhereWithoutChildrenInput = {
where?: Prisma.ProclamationCategoryWhereInput
data: Prisma.XOR<Prisma.ProclamationCategoryUpdateWithoutChildrenInput, Prisma.ProclamationCategoryUncheckedUpdateWithoutChildrenInput>
}
export type ProclamationCategoryUpdateWithoutChildrenInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
parent?: Prisma.ProclamationCategoryUpdateOneWithoutChildrenNestedInput
proclamations?: Prisma.ProclamationUpdateManyWithoutCategoryNestedInput
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUpdateManyWithoutCategoryNestedInput
}
export type ProclamationCategoryUncheckedUpdateWithoutChildrenInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number
name?: Prisma.StringFieldUpdateOperationsInput | string
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
parentId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
proclamations?: Prisma.ProclamationUncheckedUpdateManyWithoutCategoryNestedInput
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUncheckedUpdateManyWithoutCategoryNestedInput
}
export type ProclamationCategoryUpsertWithWhereUniqueWithoutParentInput = {
where: Prisma.ProclamationCategoryWhereUniqueInput
update: Prisma.XOR<Prisma.ProclamationCategoryUpdateWithoutParentInput, Prisma.ProclamationCategoryUncheckedUpdateWithoutParentInput>
create: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutParentInput, Prisma.ProclamationCategoryUncheckedCreateWithoutParentInput>
}
export type ProclamationCategoryUpdateWithWhereUniqueWithoutParentInput = {
where: Prisma.ProclamationCategoryWhereUniqueInput
data: Prisma.XOR<Prisma.ProclamationCategoryUpdateWithoutParentInput, Prisma.ProclamationCategoryUncheckedUpdateWithoutParentInput>
}
export type ProclamationCategoryUpdateManyWithWhereWithoutParentInput = {
where: Prisma.ProclamationCategoryScalarWhereInput
data: Prisma.XOR<Prisma.ProclamationCategoryUpdateManyMutationInput, Prisma.ProclamationCategoryUncheckedUpdateManyWithoutParentInput>
}
export type ProclamationCategoryScalarWhereInput = {
AND?: Prisma.ProclamationCategoryScalarWhereInput | Prisma.ProclamationCategoryScalarWhereInput[]
OR?: Prisma.ProclamationCategoryScalarWhereInput[]
NOT?: Prisma.ProclamationCategoryScalarWhereInput | Prisma.ProclamationCategoryScalarWhereInput[]
id?: Prisma.IntFilter<"ProclamationCategory"> | number
name?: Prisma.StringFilter<"ProclamationCategory"> | string
sortOrder?: Prisma.IntFilter<"ProclamationCategory"> | number
parentId?: Prisma.IntNullableFilter<"ProclamationCategory"> | number | null
}
export type ProclamationCategoryCreateWithoutSubscriptionLevelsInput = {
name: string
sortOrder?: number
parent?: Prisma.ProclamationCategoryCreateNestedOneWithoutChildrenInput
children?: Prisma.ProclamationCategoryCreateNestedManyWithoutParentInput
proclamations?: Prisma.ProclamationCreateNestedManyWithoutCategoryInput
}
export type ProclamationCategoryUncheckedCreateWithoutSubscriptionLevelsInput = {
id?: number
name: string
sortOrder?: number
parentId?: number | null
children?: Prisma.ProclamationCategoryUncheckedCreateNestedManyWithoutParentInput
proclamations?: Prisma.ProclamationUncheckedCreateNestedManyWithoutCategoryInput
}
export type ProclamationCategoryCreateOrConnectWithoutSubscriptionLevelsInput = {
where: Prisma.ProclamationCategoryWhereUniqueInput
create: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutSubscriptionLevelsInput, Prisma.ProclamationCategoryUncheckedCreateWithoutSubscriptionLevelsInput>
}
export type ProclamationCategoryUpsertWithoutSubscriptionLevelsInput = {
update: Prisma.XOR<Prisma.ProclamationCategoryUpdateWithoutSubscriptionLevelsInput, Prisma.ProclamationCategoryUncheckedUpdateWithoutSubscriptionLevelsInput>
create: Prisma.XOR<Prisma.ProclamationCategoryCreateWithoutSubscriptionLevelsInput, Prisma.ProclamationCategoryUncheckedCreateWithoutSubscriptionLevelsInput>
where?: Prisma.ProclamationCategoryWhereInput
}
export type ProclamationCategoryUpdateToOneWithWhereWithoutSubscriptionLevelsInput = {
where?: Prisma.ProclamationCategoryWhereInput
data: Prisma.XOR<Prisma.ProclamationCategoryUpdateWithoutSubscriptionLevelsInput, Prisma.ProclamationCategoryUncheckedUpdateWithoutSubscriptionLevelsInput>
}
export type ProclamationCategoryUpdateWithoutSubscriptionLevelsInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
parent?: Prisma.ProclamationCategoryUpdateOneWithoutChildrenNestedInput
children?: Prisma.ProclamationCategoryUpdateManyWithoutParentNestedInput
proclamations?: Prisma.ProclamationUpdateManyWithoutCategoryNestedInput
}
export type ProclamationCategoryUncheckedUpdateWithoutSubscriptionLevelsInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number
name?: Prisma.StringFieldUpdateOperationsInput | string
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
parentId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
children?: Prisma.ProclamationCategoryUncheckedUpdateManyWithoutParentNestedInput
proclamations?: Prisma.ProclamationUncheckedUpdateManyWithoutCategoryNestedInput
}
export type ProclamationCategoryCreateManyParentInput = {
id?: number
name: string
sortOrder?: number
}
export type ProclamationCategoryUpdateWithoutParentInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
children?: Prisma.ProclamationCategoryUpdateManyWithoutParentNestedInput
proclamations?: Prisma.ProclamationUpdateManyWithoutCategoryNestedInput
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUpdateManyWithoutCategoryNestedInput
}
export type ProclamationCategoryUncheckedUpdateWithoutParentInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number
name?: Prisma.StringFieldUpdateOperationsInput | string
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
children?: Prisma.ProclamationCategoryUncheckedUpdateManyWithoutParentNestedInput
proclamations?: Prisma.ProclamationUncheckedUpdateManyWithoutCategoryNestedInput
subscriptionLevels?: Prisma.ProclamationCategorySubscriptionLevelUncheckedUpdateManyWithoutCategoryNestedInput
}
export type ProclamationCategoryUncheckedUpdateManyWithoutParentInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number
name?: Prisma.StringFieldUpdateOperationsInput | string
sortOrder?: Prisma.IntFieldUpdateOperationsInput | number
}
@@ -709,15 +378,11 @@ export type ProclamationCategoryUncheckedUpdateManyWithoutParentInput = {
*/
export type ProclamationCategoryCountOutputType = {
children: number
proclamations: number
subscriptionLevels: number
}
export type ProclamationCategoryCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
children?: boolean | ProclamationCategoryCountOutputTypeCountChildrenArgs
proclamations?: boolean | ProclamationCategoryCountOutputTypeCountProclamationsArgs
subscriptionLevels?: boolean | ProclamationCategoryCountOutputTypeCountSubscriptionLevelsArgs
}
/**
@@ -730,13 +395,6 @@ export type ProclamationCategoryCountOutputTypeDefaultArgs<ExtArgs extends runti
select?: Prisma.ProclamationCategoryCountOutputTypeSelect<ExtArgs> | null
}
/**
* ProclamationCategoryCountOutputType without action
*/
export type ProclamationCategoryCountOutputTypeCountChildrenArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.ProclamationCategoryWhereInput
}
/**
* ProclamationCategoryCountOutputType without action
*/
@@ -744,23 +402,12 @@ export type ProclamationCategoryCountOutputTypeCountProclamationsArgs<ExtArgs ex
where?: Prisma.ProclamationWhereInput
}
/**
* ProclamationCategoryCountOutputType without action
*/
export type ProclamationCategoryCountOutputTypeCountSubscriptionLevelsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.ProclamationCategorySubscriptionLevelWhereInput
}
export type ProclamationCategorySelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
name?: boolean
sortOrder?: boolean
parentId?: boolean
parent?: boolean | Prisma.ProclamationCategory$parentArgs<ExtArgs>
children?: boolean | Prisma.ProclamationCategory$childrenArgs<ExtArgs>
proclamations?: boolean | Prisma.ProclamationCategory$proclamationsArgs<ExtArgs>
subscriptionLevels?: boolean | Prisma.ProclamationCategory$subscriptionLevelsArgs<ExtArgs>
_count?: boolean | Prisma.ProclamationCategoryCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["proclamationCategory"]>
@@ -768,53 +415,37 @@ export type ProclamationCategorySelectCreateManyAndReturn<ExtArgs extends runtim
id?: boolean
name?: boolean
sortOrder?: boolean
parentId?: boolean
parent?: boolean | Prisma.ProclamationCategory$parentArgs<ExtArgs>
}, ExtArgs["result"]["proclamationCategory"]>
export type ProclamationCategorySelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
name?: boolean
sortOrder?: boolean
parentId?: boolean
parent?: boolean | Prisma.ProclamationCategory$parentArgs<ExtArgs>
}, ExtArgs["result"]["proclamationCategory"]>
export type ProclamationCategorySelectScalar = {
id?: boolean
name?: boolean
sortOrder?: boolean
parentId?: boolean
}
export type ProclamationCategoryOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "sortOrder" | "parentId", ExtArgs["result"]["proclamationCategory"]>
export type ProclamationCategoryOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "sortOrder", ExtArgs["result"]["proclamationCategory"]>
export type ProclamationCategoryInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
parent?: boolean | Prisma.ProclamationCategory$parentArgs<ExtArgs>
children?: boolean | Prisma.ProclamationCategory$childrenArgs<ExtArgs>
proclamations?: boolean | Prisma.ProclamationCategory$proclamationsArgs<ExtArgs>
subscriptionLevels?: boolean | Prisma.ProclamationCategory$subscriptionLevelsArgs<ExtArgs>
_count?: boolean | Prisma.ProclamationCategoryCountOutputTypeDefaultArgs<ExtArgs>
}
export type ProclamationCategoryIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
parent?: boolean | Prisma.ProclamationCategory$parentArgs<ExtArgs>
}
export type ProclamationCategoryIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
parent?: boolean | Prisma.ProclamationCategory$parentArgs<ExtArgs>
}
export type ProclamationCategoryIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {}
export type ProclamationCategoryIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {}
export type $ProclamationCategoryPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "ProclamationCategory"
objects: {
parent: Prisma.$ProclamationCategoryPayload<ExtArgs> | null
children: Prisma.$ProclamationCategoryPayload<ExtArgs>[]
proclamations: Prisma.$ProclamationPayload<ExtArgs>[]
subscriptionLevels: Prisma.$ProclamationCategorySubscriptionLevelPayload<ExtArgs>[]
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: number
name: string
sortOrder: number
parentId: number | null
}, ExtArgs["result"]["proclamationCategory"]>
composites: {}
}
@@ -1209,10 +840,7 @@ readonly fields: ProclamationCategoryFieldRefs;
*/
export interface Prisma__ProclamationCategoryClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
readonly [Symbol.toStringTag]: "PrismaPromise"
parent<T extends Prisma.ProclamationCategory$parentArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.ProclamationCategory$parentArgs<ExtArgs>>): Prisma.Prisma__ProclamationCategoryClient<runtime.Types.Result.GetResult<Prisma.$ProclamationCategoryPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
children<T extends Prisma.ProclamationCategory$childrenArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.ProclamationCategory$childrenArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ProclamationCategoryPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
proclamations<T extends Prisma.ProclamationCategory$proclamationsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.ProclamationCategory$proclamationsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ProclamationPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
subscriptionLevels<T extends Prisma.ProclamationCategory$subscriptionLevelsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.ProclamationCategory$subscriptionLevelsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ProclamationCategorySubscriptionLevelPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
@@ -1245,7 +873,6 @@ export interface ProclamationCategoryFieldRefs {
readonly id: Prisma.FieldRef<"ProclamationCategory", 'Int'>
readonly name: Prisma.FieldRef<"ProclamationCategory", 'String'>
readonly sortOrder: Prisma.FieldRef<"ProclamationCategory", 'Int'>
readonly parentId: Prisma.FieldRef<"ProclamationCategory", 'Int'>
}
@@ -1500,10 +1127,6 @@ export type ProclamationCategoryCreateManyAndReturnArgs<ExtArgs extends runtime.
*/
data: Prisma.ProclamationCategoryCreateManyInput | Prisma.ProclamationCategoryCreateManyInput[]
skipDuplicates?: boolean
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.ProclamationCategoryIncludeCreateManyAndReturn<ExtArgs> | null
}
/**
@@ -1574,10 +1197,6 @@ export type ProclamationCategoryUpdateManyAndReturnArgs<ExtArgs extends runtime.
* Limit how many ProclamationCategories to update.
*/
limit?: number
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.ProclamationCategoryIncludeUpdateManyAndReturn<ExtArgs> | null
}
/**
@@ -1646,49 +1265,6 @@ export type ProclamationCategoryDeleteManyArgs<ExtArgs extends runtime.Types.Ext
limit?: number
}
/**
* ProclamationCategory.parent
*/
export type ProclamationCategory$parentArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the ProclamationCategory
*/
select?: Prisma.ProclamationCategorySelect<ExtArgs> | null
/**
* Omit specific fields from the ProclamationCategory
*/
omit?: Prisma.ProclamationCategoryOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.ProclamationCategoryInclude<ExtArgs> | null
where?: Prisma.ProclamationCategoryWhereInput
}
/**
* ProclamationCategory.children
*/
export type ProclamationCategory$childrenArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the ProclamationCategory
*/
select?: Prisma.ProclamationCategorySelect<ExtArgs> | null
/**
* Omit specific fields from the ProclamationCategory
*/
omit?: Prisma.ProclamationCategoryOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.ProclamationCategoryInclude<ExtArgs> | null
where?: Prisma.ProclamationCategoryWhereInput
orderBy?: Prisma.ProclamationCategoryOrderByWithRelationInput | Prisma.ProclamationCategoryOrderByWithRelationInput[]
cursor?: Prisma.ProclamationCategoryWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.ProclamationCategoryScalarFieldEnum | Prisma.ProclamationCategoryScalarFieldEnum[]
}
/**
* ProclamationCategory.proclamations
*/
@@ -1713,30 +1289,6 @@ export type ProclamationCategory$proclamationsArgs<ExtArgs extends runtime.Types
distinct?: Prisma.ProclamationScalarFieldEnum | Prisma.ProclamationScalarFieldEnum[]
}
/**
* ProclamationCategory.subscriptionLevels
*/
export type ProclamationCategory$subscriptionLevelsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the ProclamationCategorySubscriptionLevel
*/
select?: Prisma.ProclamationCategorySubscriptionLevelSelect<ExtArgs> | null
/**
* Omit specific fields from the ProclamationCategorySubscriptionLevel
*/
omit?: Prisma.ProclamationCategorySubscriptionLevelOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.ProclamationCategorySubscriptionLevelInclude<ExtArgs> | null
where?: Prisma.ProclamationCategorySubscriptionLevelWhereInput
orderBy?: Prisma.ProclamationCategorySubscriptionLevelOrderByWithRelationInput | Prisma.ProclamationCategorySubscriptionLevelOrderByWithRelationInput[]
cursor?: Prisma.ProclamationCategorySubscriptionLevelWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.ProclamationCategorySubscriptionLevelScalarFieldEnum | Prisma.ProclamationCategorySubscriptionLevelScalarFieldEnum[]
}
/**
* ProclamationCategory without action
*/
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+187 -76
View File
@@ -27,6 +27,7 @@ export type AggregateProfile = {
export type ProfileMinAggregateOutputType = {
id: string | null
userId: string | null
email: string | null
firstName: string | null
lastName: string | null
phoneNumber: string | null
@@ -36,6 +37,7 @@ export type ProfileMinAggregateOutputType = {
export type ProfileMaxAggregateOutputType = {
id: string | null
userId: string | null
email: string | null
firstName: string | null
lastName: string | null
phoneNumber: string | null
@@ -45,6 +47,7 @@ export type ProfileMaxAggregateOutputType = {
export type ProfileCountAggregateOutputType = {
id: number
userId: number
email: number
firstName: number
lastName: number
phoneNumber: number
@@ -56,6 +59,7 @@ export type ProfileCountAggregateOutputType = {
export type ProfileMinAggregateInputType = {
id?: true
userId?: true
email?: true
firstName?: true
lastName?: true
phoneNumber?: true
@@ -65,6 +69,7 @@ export type ProfileMinAggregateInputType = {
export type ProfileMaxAggregateInputType = {
id?: true
userId?: true
email?: true
firstName?: true
lastName?: true
phoneNumber?: true
@@ -74,6 +79,7 @@ export type ProfileMaxAggregateInputType = {
export type ProfileCountAggregateInputType = {
id?: true
userId?: true
email?: true
firstName?: true
lastName?: true
phoneNumber?: true
@@ -155,7 +161,8 @@ export type ProfileGroupByArgs<ExtArgs extends runtime.Types.Extensions.Internal
export type ProfileGroupByOutputType = {
id: string
userId: string
userId: string | null
email: string | null
firstName: string
lastName: string
phoneNumber: string
@@ -185,18 +192,20 @@ export type ProfileWhereInput = {
OR?: Prisma.ProfileWhereInput[]
NOT?: Prisma.ProfileWhereInput | Prisma.ProfileWhereInput[]
id?: Prisma.StringFilter<"Profile"> | string
userId?: Prisma.StringFilter<"Profile"> | string
userId?: Prisma.StringNullableFilter<"Profile"> | string | null
email?: Prisma.StringNullableFilter<"Profile"> | string | null
firstName?: Prisma.StringFilter<"Profile"> | string
lastName?: Prisma.StringFilter<"Profile"> | string
phoneNumber?: Prisma.StringFilter<"Profile"> | string
ghlContactId?: Prisma.StringNullableFilter<"Profile"> | string | null
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
user?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
addresses?: Prisma.AddressListRelationFilter
}
export type ProfileOrderByWithRelationInput = {
id?: Prisma.SortOrder
userId?: Prisma.SortOrder
userId?: Prisma.SortOrderInput | Prisma.SortOrder
email?: Prisma.SortOrderInput | Prisma.SortOrder
firstName?: Prisma.SortOrder
lastName?: Prisma.SortOrder
phoneNumber?: Prisma.SortOrder
@@ -207,21 +216,23 @@ export type ProfileOrderByWithRelationInput = {
export type ProfileWhereUniqueInput = Prisma.AtLeast<{
id?: string
userId?: string
AND?: Prisma.ProfileWhereInput | Prisma.ProfileWhereInput[]
OR?: Prisma.ProfileWhereInput[]
NOT?: Prisma.ProfileWhereInput | Prisma.ProfileWhereInput[]
userId?: Prisma.StringNullableFilter<"Profile"> | string | null
email?: Prisma.StringNullableFilter<"Profile"> | string | null
firstName?: Prisma.StringFilter<"Profile"> | string
lastName?: Prisma.StringFilter<"Profile"> | string
phoneNumber?: Prisma.StringFilter<"Profile"> | string
ghlContactId?: Prisma.StringNullableFilter<"Profile"> | string | null
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
user?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
addresses?: Prisma.AddressListRelationFilter
}, "id" | "userId">
}, "id">
export type ProfileOrderByWithAggregationInput = {
id?: Prisma.SortOrder
userId?: Prisma.SortOrder
userId?: Prisma.SortOrderInput | Prisma.SortOrder
email?: Prisma.SortOrderInput | Prisma.SortOrder
firstName?: Prisma.SortOrder
lastName?: Prisma.SortOrder
phoneNumber?: Prisma.SortOrder
@@ -236,7 +247,8 @@ export type ProfileScalarWhereWithAggregatesInput = {
OR?: Prisma.ProfileScalarWhereWithAggregatesInput[]
NOT?: Prisma.ProfileScalarWhereWithAggregatesInput | Prisma.ProfileScalarWhereWithAggregatesInput[]
id?: Prisma.StringWithAggregatesFilter<"Profile"> | string
userId?: Prisma.StringWithAggregatesFilter<"Profile"> | string
userId?: Prisma.StringNullableWithAggregatesFilter<"Profile"> | string | null
email?: Prisma.StringNullableWithAggregatesFilter<"Profile"> | string | null
firstName?: Prisma.StringWithAggregatesFilter<"Profile"> | string
lastName?: Prisma.StringWithAggregatesFilter<"Profile"> | string
phoneNumber?: Prisma.StringWithAggregatesFilter<"Profile"> | string
@@ -245,17 +257,19 @@ export type ProfileScalarWhereWithAggregatesInput = {
export type ProfileCreateInput = {
id?: string
email?: string | null
firstName: string
lastName: string
phoneNumber: string
ghlContactId?: string | null
user: Prisma.UserCreateNestedOneWithoutProfileInput
user?: Prisma.UserCreateNestedOneWithoutProfilesInput
addresses?: Prisma.AddressCreateNestedManyWithoutProfileInput
}
export type ProfileUncheckedCreateInput = {
id?: string
userId: string
userId?: string | null
email?: string | null
firstName: string
lastName: string
phoneNumber: string
@@ -265,17 +279,19 @@ export type ProfileUncheckedCreateInput = {
export type ProfileUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
user?: Prisma.UserUpdateOneRequiredWithoutProfileNestedInput
user?: Prisma.UserUpdateOneWithoutProfilesNestedInput
addresses?: Prisma.AddressUpdateManyWithoutProfileNestedInput
}
export type ProfileUncheckedUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
userId?: Prisma.StringFieldUpdateOperationsInput | string
userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
@@ -285,7 +301,8 @@ export type ProfileUncheckedUpdateInput = {
export type ProfileCreateManyInput = {
id?: string
userId: string
userId?: string | null
email?: string | null
firstName: string
lastName: string
phoneNumber: string
@@ -294,6 +311,7 @@ export type ProfileCreateManyInput = {
export type ProfileUpdateManyMutationInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
@@ -302,21 +320,28 @@ export type ProfileUpdateManyMutationInput = {
export type ProfileUncheckedUpdateManyInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
userId?: Prisma.StringFieldUpdateOperationsInput | string
userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type ProfileNullableScalarRelationFilter = {
is?: Prisma.ProfileWhereInput | null
isNot?: Prisma.ProfileWhereInput | null
export type ProfileListRelationFilter = {
every?: Prisma.ProfileWhereInput
some?: Prisma.ProfileWhereInput
none?: Prisma.ProfileWhereInput
}
export type ProfileOrderByRelationAggregateInput = {
_count?: Prisma.SortOrder
}
export type ProfileCountOrderByAggregateInput = {
id?: Prisma.SortOrder
userId?: Prisma.SortOrder
email?: Prisma.SortOrder
firstName?: Prisma.SortOrder
lastName?: Prisma.SortOrder
phoneNumber?: Prisma.SortOrder
@@ -326,6 +351,7 @@ export type ProfileCountOrderByAggregateInput = {
export type ProfileMaxOrderByAggregateInput = {
id?: Prisma.SortOrder
userId?: Prisma.SortOrder
email?: Prisma.SortOrder
firstName?: Prisma.SortOrder
lastName?: Prisma.SortOrder
phoneNumber?: Prisma.SortOrder
@@ -335,6 +361,7 @@ export type ProfileMaxOrderByAggregateInput = {
export type ProfileMinOrderByAggregateInput = {
id?: Prisma.SortOrder
userId?: Prisma.SortOrder
email?: Prisma.SortOrder
firstName?: Prisma.SortOrder
lastName?: Prisma.SortOrder
phoneNumber?: Prisma.SortOrder
@@ -346,36 +373,46 @@ export type ProfileScalarRelationFilter = {
isNot?: Prisma.ProfileWhereInput
}
export type ProfileCreateNestedOneWithoutUserInput = {
create?: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput>
connectOrCreate?: Prisma.ProfileCreateOrConnectWithoutUserInput
connect?: Prisma.ProfileWhereUniqueInput
export type ProfileCreateNestedManyWithoutUserInput = {
create?: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput> | Prisma.ProfileCreateWithoutUserInput[] | Prisma.ProfileUncheckedCreateWithoutUserInput[]
connectOrCreate?: Prisma.ProfileCreateOrConnectWithoutUserInput | Prisma.ProfileCreateOrConnectWithoutUserInput[]
createMany?: Prisma.ProfileCreateManyUserInputEnvelope
connect?: Prisma.ProfileWhereUniqueInput | Prisma.ProfileWhereUniqueInput[]
}
export type ProfileUncheckedCreateNestedOneWithoutUserInput = {
create?: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput>
connectOrCreate?: Prisma.ProfileCreateOrConnectWithoutUserInput
connect?: Prisma.ProfileWhereUniqueInput
export type ProfileUncheckedCreateNestedManyWithoutUserInput = {
create?: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput> | Prisma.ProfileCreateWithoutUserInput[] | Prisma.ProfileUncheckedCreateWithoutUserInput[]
connectOrCreate?: Prisma.ProfileCreateOrConnectWithoutUserInput | Prisma.ProfileCreateOrConnectWithoutUserInput[]
createMany?: Prisma.ProfileCreateManyUserInputEnvelope
connect?: Prisma.ProfileWhereUniqueInput | Prisma.ProfileWhereUniqueInput[]
}
export type ProfileUpdateOneWithoutUserNestedInput = {
create?: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput>
connectOrCreate?: Prisma.ProfileCreateOrConnectWithoutUserInput
upsert?: Prisma.ProfileUpsertWithoutUserInput
disconnect?: Prisma.ProfileWhereInput | boolean
delete?: Prisma.ProfileWhereInput | boolean
connect?: Prisma.ProfileWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.ProfileUpdateToOneWithWhereWithoutUserInput, Prisma.ProfileUpdateWithoutUserInput>, Prisma.ProfileUncheckedUpdateWithoutUserInput>
export type ProfileUpdateManyWithoutUserNestedInput = {
create?: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput> | Prisma.ProfileCreateWithoutUserInput[] | Prisma.ProfileUncheckedCreateWithoutUserInput[]
connectOrCreate?: Prisma.ProfileCreateOrConnectWithoutUserInput | Prisma.ProfileCreateOrConnectWithoutUserInput[]
upsert?: Prisma.ProfileUpsertWithWhereUniqueWithoutUserInput | Prisma.ProfileUpsertWithWhereUniqueWithoutUserInput[]
createMany?: Prisma.ProfileCreateManyUserInputEnvelope
set?: Prisma.ProfileWhereUniqueInput | Prisma.ProfileWhereUniqueInput[]
disconnect?: Prisma.ProfileWhereUniqueInput | Prisma.ProfileWhereUniqueInput[]
delete?: Prisma.ProfileWhereUniqueInput | Prisma.ProfileWhereUniqueInput[]
connect?: Prisma.ProfileWhereUniqueInput | Prisma.ProfileWhereUniqueInput[]
update?: Prisma.ProfileUpdateWithWhereUniqueWithoutUserInput | Prisma.ProfileUpdateWithWhereUniqueWithoutUserInput[]
updateMany?: Prisma.ProfileUpdateManyWithWhereWithoutUserInput | Prisma.ProfileUpdateManyWithWhereWithoutUserInput[]
deleteMany?: Prisma.ProfileScalarWhereInput | Prisma.ProfileScalarWhereInput[]
}
export type ProfileUncheckedUpdateOneWithoutUserNestedInput = {
create?: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput>
connectOrCreate?: Prisma.ProfileCreateOrConnectWithoutUserInput
upsert?: Prisma.ProfileUpsertWithoutUserInput
disconnect?: Prisma.ProfileWhereInput | boolean
delete?: Prisma.ProfileWhereInput | boolean
connect?: Prisma.ProfileWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.ProfileUpdateToOneWithWhereWithoutUserInput, Prisma.ProfileUpdateWithoutUserInput>, Prisma.ProfileUncheckedUpdateWithoutUserInput>
export type ProfileUncheckedUpdateManyWithoutUserNestedInput = {
create?: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput> | Prisma.ProfileCreateWithoutUserInput[] | Prisma.ProfileUncheckedCreateWithoutUserInput[]
connectOrCreate?: Prisma.ProfileCreateOrConnectWithoutUserInput | Prisma.ProfileCreateOrConnectWithoutUserInput[]
upsert?: Prisma.ProfileUpsertWithWhereUniqueWithoutUserInput | Prisma.ProfileUpsertWithWhereUniqueWithoutUserInput[]
createMany?: Prisma.ProfileCreateManyUserInputEnvelope
set?: Prisma.ProfileWhereUniqueInput | Prisma.ProfileWhereUniqueInput[]
disconnect?: Prisma.ProfileWhereUniqueInput | Prisma.ProfileWhereUniqueInput[]
delete?: Prisma.ProfileWhereUniqueInput | Prisma.ProfileWhereUniqueInput[]
connect?: Prisma.ProfileWhereUniqueInput | Prisma.ProfileWhereUniqueInput[]
update?: Prisma.ProfileUpdateWithWhereUniqueWithoutUserInput | Prisma.ProfileUpdateWithWhereUniqueWithoutUserInput[]
updateMany?: Prisma.ProfileUpdateManyWithWhereWithoutUserInput | Prisma.ProfileUpdateManyWithWhereWithoutUserInput[]
deleteMany?: Prisma.ProfileScalarWhereInput | Prisma.ProfileScalarWhereInput[]
}
export type ProfileCreateNestedOneWithoutAddressesInput = {
@@ -394,6 +431,7 @@ export type ProfileUpdateOneRequiredWithoutAddressesNestedInput = {
export type ProfileCreateWithoutUserInput = {
id?: string
email?: string | null
firstName: string
lastName: string
phoneNumber: string
@@ -403,6 +441,7 @@ export type ProfileCreateWithoutUserInput = {
export type ProfileUncheckedCreateWithoutUserInput = {
id?: string
email?: string | null
firstName: string
lastName: string
phoneNumber: string
@@ -415,47 +454,54 @@ export type ProfileCreateOrConnectWithoutUserInput = {
create: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput>
}
export type ProfileUpsertWithoutUserInput = {
update: Prisma.XOR<Prisma.ProfileUpdateWithoutUserInput, Prisma.ProfileUncheckedUpdateWithoutUserInput>
create: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput>
where?: Prisma.ProfileWhereInput
export type ProfileCreateManyUserInputEnvelope = {
data: Prisma.ProfileCreateManyUserInput | Prisma.ProfileCreateManyUserInput[]
skipDuplicates?: boolean
}
export type ProfileUpdateToOneWithWhereWithoutUserInput = {
where?: Prisma.ProfileWhereInput
export type ProfileUpsertWithWhereUniqueWithoutUserInput = {
where: Prisma.ProfileWhereUniqueInput
update: Prisma.XOR<Prisma.ProfileUpdateWithoutUserInput, Prisma.ProfileUncheckedUpdateWithoutUserInput>
create: Prisma.XOR<Prisma.ProfileCreateWithoutUserInput, Prisma.ProfileUncheckedCreateWithoutUserInput>
}
export type ProfileUpdateWithWhereUniqueWithoutUserInput = {
where: Prisma.ProfileWhereUniqueInput
data: Prisma.XOR<Prisma.ProfileUpdateWithoutUserInput, Prisma.ProfileUncheckedUpdateWithoutUserInput>
}
export type ProfileUpdateWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
addresses?: Prisma.AddressUpdateManyWithoutProfileNestedInput
export type ProfileUpdateManyWithWhereWithoutUserInput = {
where: Prisma.ProfileScalarWhereInput
data: Prisma.XOR<Prisma.ProfileUpdateManyMutationInput, Prisma.ProfileUncheckedUpdateManyWithoutUserInput>
}
export type ProfileUncheckedUpdateWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
addresses?: Prisma.AddressUncheckedUpdateManyWithoutProfileNestedInput
export type ProfileScalarWhereInput = {
AND?: Prisma.ProfileScalarWhereInput | Prisma.ProfileScalarWhereInput[]
OR?: Prisma.ProfileScalarWhereInput[]
NOT?: Prisma.ProfileScalarWhereInput | Prisma.ProfileScalarWhereInput[]
id?: Prisma.StringFilter<"Profile"> | string
userId?: Prisma.StringNullableFilter<"Profile"> | string | null
email?: Prisma.StringNullableFilter<"Profile"> | string | null
firstName?: Prisma.StringFilter<"Profile"> | string
lastName?: Prisma.StringFilter<"Profile"> | string
phoneNumber?: Prisma.StringFilter<"Profile"> | string
ghlContactId?: Prisma.StringNullableFilter<"Profile"> | string | null
}
export type ProfileCreateWithoutAddressesInput = {
id?: string
email?: string | null
firstName: string
lastName: string
phoneNumber: string
ghlContactId?: string | null
user: Prisma.UserCreateNestedOneWithoutProfileInput
user?: Prisma.UserCreateNestedOneWithoutProfilesInput
}
export type ProfileUncheckedCreateWithoutAddressesInput = {
id?: string
userId: string
userId?: string | null
email?: string | null
firstName: string
lastName: string
phoneNumber: string
@@ -480,16 +526,56 @@ export type ProfileUpdateToOneWithWhereWithoutAddressesInput = {
export type ProfileUpdateWithoutAddressesInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
user?: Prisma.UserUpdateOneRequiredWithoutProfileNestedInput
user?: Prisma.UserUpdateOneWithoutProfilesNestedInput
}
export type ProfileUncheckedUpdateWithoutAddressesInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
userId?: Prisma.StringFieldUpdateOperationsInput | string
userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type ProfileCreateManyUserInput = {
id?: string
email?: string | null
firstName: string
lastName: string
phoneNumber: string
ghlContactId?: string | null
}
export type ProfileUpdateWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
addresses?: Prisma.AddressUpdateManyWithoutProfileNestedInput
}
export type ProfileUncheckedUpdateWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
addresses?: Prisma.AddressUncheckedUpdateManyWithoutProfileNestedInput
}
export type ProfileUncheckedUpdateManyWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
@@ -530,11 +616,12 @@ export type ProfileCountOutputTypeCountAddressesArgs<ExtArgs extends runtime.Typ
export type ProfileSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
userId?: boolean
email?: boolean
firstName?: boolean
lastName?: boolean
phoneNumber?: boolean
ghlContactId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
user?: boolean | Prisma.Profile$userArgs<ExtArgs>
addresses?: boolean | Prisma.Profile$addressesArgs<ExtArgs>
_count?: boolean | Prisma.ProfileCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["profile"]>
@@ -542,54 +629,58 @@ export type ProfileSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
export type ProfileSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
userId?: boolean
email?: boolean
firstName?: boolean
lastName?: boolean
phoneNumber?: boolean
ghlContactId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
user?: boolean | Prisma.Profile$userArgs<ExtArgs>
}, ExtArgs["result"]["profile"]>
export type ProfileSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
userId?: boolean
email?: boolean
firstName?: boolean
lastName?: boolean
phoneNumber?: boolean
ghlContactId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
user?: boolean | Prisma.Profile$userArgs<ExtArgs>
}, ExtArgs["result"]["profile"]>
export type ProfileSelectScalar = {
id?: boolean
userId?: boolean
email?: boolean
firstName?: boolean
lastName?: boolean
phoneNumber?: boolean
ghlContactId?: boolean
}
export type ProfileOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "userId" | "firstName" | "lastName" | "phoneNumber" | "ghlContactId", ExtArgs["result"]["profile"]>
export type ProfileOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "userId" | "email" | "firstName" | "lastName" | "phoneNumber" | "ghlContactId", ExtArgs["result"]["profile"]>
export type ProfileInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
user?: boolean | Prisma.Profile$userArgs<ExtArgs>
addresses?: boolean | Prisma.Profile$addressesArgs<ExtArgs>
_count?: boolean | Prisma.ProfileCountOutputTypeDefaultArgs<ExtArgs>
}
export type ProfileIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
user?: boolean | Prisma.Profile$userArgs<ExtArgs>
}
export type ProfileIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
user?: boolean | Prisma.Profile$userArgs<ExtArgs>
}
export type $ProfilePayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "Profile"
objects: {
user: Prisma.$UserPayload<ExtArgs>
user: Prisma.$UserPayload<ExtArgs> | null
addresses: Prisma.$AddressPayload<ExtArgs>[]
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string
userId: string
userId: string | null
email: string | null
firstName: string
lastName: string
phoneNumber: string
@@ -988,7 +1079,7 @@ readonly fields: ProfileFieldRefs;
*/
export interface Prisma__ProfileClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
readonly [Symbol.toStringTag]: "PrismaPromise"
user<T extends Prisma.UserDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.UserDefaultArgs<ExtArgs>>): Prisma.Prisma__UserClient<runtime.Types.Result.GetResult<Prisma.$UserPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
user<T extends Prisma.Profile$userArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Profile$userArgs<ExtArgs>>): Prisma.Prisma__UserClient<runtime.Types.Result.GetResult<Prisma.$UserPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
addresses<T extends Prisma.Profile$addressesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Profile$addressesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$AddressPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
@@ -1021,6 +1112,7 @@ export interface Prisma__ProfileClient<T, Null = never, ExtArgs extends runtime.
export interface ProfileFieldRefs {
readonly id: Prisma.FieldRef<"Profile", 'String'>
readonly userId: Prisma.FieldRef<"Profile", 'String'>
readonly email: Prisma.FieldRef<"Profile", 'String'>
readonly firstName: Prisma.FieldRef<"Profile", 'String'>
readonly lastName: Prisma.FieldRef<"Profile", 'String'>
readonly phoneNumber: Prisma.FieldRef<"Profile", 'String'>
@@ -1425,6 +1517,25 @@ export type ProfileDeleteManyArgs<ExtArgs extends runtime.Types.Extensions.Inter
limit?: number
}
/**
* Profile.user
*/
export type Profile$userArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the User
*/
select?: Prisma.UserSelect<ExtArgs> | null
/**
* Omit specific fields from the User
*/
omit?: Prisma.UserOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.UserInclude<ExtArgs> | null
where?: Prisma.UserWhereInput
}
/**
* Profile.addresses
*/
File diff suppressed because it is too large Load Diff
+76 -53
View File
@@ -243,7 +243,7 @@ export type UserWhereInput = {
role?: Prisma.XOR<Prisma.UserRoleNullableScalarRelationFilter, Prisma.UserRoleWhereInput> | null
sessions?: Prisma.SessionListRelationFilter
accounts?: Prisma.AccountListRelationFilter
profile?: Prisma.XOR<Prisma.ProfileNullableScalarRelationFilter, Prisma.ProfileWhereInput> | null
profiles?: Prisma.ProfileListRelationFilter
licenseKeys?: Prisma.LicenseKeyListRelationFilter
userProclamations?: Prisma.UserProclamationsListRelationFilter
}
@@ -260,7 +260,7 @@ export type UserOrderByWithRelationInput = {
role?: Prisma.UserRoleOrderByWithRelationInput
sessions?: Prisma.SessionOrderByRelationAggregateInput
accounts?: Prisma.AccountOrderByRelationAggregateInput
profile?: Prisma.ProfileOrderByWithRelationInput
profiles?: Prisma.ProfileOrderByRelationAggregateInput
licenseKeys?: Prisma.LicenseKeyOrderByRelationAggregateInput
userProclamations?: Prisma.UserProclamationsOrderByRelationAggregateInput
}
@@ -280,7 +280,7 @@ export type UserWhereUniqueInput = Prisma.AtLeast<{
role?: Prisma.XOR<Prisma.UserRoleNullableScalarRelationFilter, Prisma.UserRoleWhereInput> | null
sessions?: Prisma.SessionListRelationFilter
accounts?: Prisma.AccountListRelationFilter
profile?: Prisma.XOR<Prisma.ProfileNullableScalarRelationFilter, Prisma.ProfileWhereInput> | null
profiles?: Prisma.ProfileListRelationFilter
licenseKeys?: Prisma.LicenseKeyListRelationFilter
userProclamations?: Prisma.UserProclamationsListRelationFilter
}, "id" | "email">
@@ -326,7 +326,7 @@ export type UserCreateInput = {
role?: Prisma.UserRoleCreateNestedOneWithoutUsersInput
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileCreateNestedOneWithoutUserInput
profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput
}
@@ -342,7 +342,7 @@ export type UserUncheckedCreateInput = {
roleId?: number | null
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileUncheckedCreateNestedOneWithoutUserInput
profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput
}
@@ -358,7 +358,7 @@ export type UserUpdateInput = {
role?: Prisma.UserRoleUpdateOneWithoutUsersNestedInput
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUpdateOneWithoutUserNestedInput
profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput
}
@@ -374,7 +374,7 @@ export type UserUncheckedUpdateInput = {
roleId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUncheckedUpdateOneWithoutUserNestedInput
profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput
}
@@ -452,6 +452,11 @@ export type UserSumOrderByAggregateInput = {
roleId?: Prisma.SortOrder
}
export type UserNullableScalarRelationFilter = {
is?: Prisma.UserWhereInput | null
isNot?: Prisma.UserWhereInput | null
}
export type UserScalarRelationFilter = {
is?: Prisma.UserWhereInput
isNot?: Prisma.UserWhereInput
@@ -491,18 +496,20 @@ export type NullableIntFieldUpdateOperationsInput = {
divide?: number
}
export type UserCreateNestedOneWithoutProfileInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutProfileInput, Prisma.UserUncheckedCreateWithoutProfileInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutProfileInput
export type UserCreateNestedOneWithoutProfilesInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutProfilesInput, Prisma.UserUncheckedCreateWithoutProfilesInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutProfilesInput
connect?: Prisma.UserWhereUniqueInput
}
export type UserUpdateOneRequiredWithoutProfileNestedInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutProfileInput, Prisma.UserUncheckedCreateWithoutProfileInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutProfileInput
upsert?: Prisma.UserUpsertWithoutProfileInput
export type UserUpdateOneWithoutProfilesNestedInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutProfilesInput, Prisma.UserUncheckedCreateWithoutProfilesInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutProfilesInput
upsert?: Prisma.UserUpsertWithoutProfilesInput
disconnect?: Prisma.UserWhereInput | boolean
delete?: Prisma.UserWhereInput | boolean
connect?: Prisma.UserWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutProfileInput, Prisma.UserUpdateWithoutProfileInput>, Prisma.UserUncheckedUpdateWithoutProfileInput>
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutProfilesInput, Prisma.UserUpdateWithoutProfilesInput>, Prisma.UserUncheckedUpdateWithoutProfilesInput>
}
export type UserCreateNestedOneWithoutSessionsInput = {
@@ -595,15 +602,17 @@ export type UserCreateNestedOneWithoutLicenseKeysInput = {
connect?: Prisma.UserWhereUniqueInput
}
export type UserUpdateOneRequiredWithoutLicenseKeysNestedInput = {
export type UserUpdateOneWithoutLicenseKeysNestedInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutLicenseKeysInput, Prisma.UserUncheckedCreateWithoutLicenseKeysInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutLicenseKeysInput
upsert?: Prisma.UserUpsertWithoutLicenseKeysInput
disconnect?: Prisma.UserWhereInput | boolean
delete?: Prisma.UserWhereInput | boolean
connect?: Prisma.UserWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutLicenseKeysInput, Prisma.UserUpdateWithoutLicenseKeysInput>, Prisma.UserUncheckedUpdateWithoutLicenseKeysInput>
}
export type UserCreateWithoutProfileInput = {
export type UserCreateWithoutProfilesInput = {
id: string
name: string
email: string
@@ -618,7 +627,7 @@ export type UserCreateWithoutProfileInput = {
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput
}
export type UserUncheckedCreateWithoutProfileInput = {
export type UserUncheckedCreateWithoutProfilesInput = {
id: string
name: string
email: string
@@ -633,23 +642,23 @@ export type UserUncheckedCreateWithoutProfileInput = {
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput
}
export type UserCreateOrConnectWithoutProfileInput = {
export type UserCreateOrConnectWithoutProfilesInput = {
where: Prisma.UserWhereUniqueInput
create: Prisma.XOR<Prisma.UserCreateWithoutProfileInput, Prisma.UserUncheckedCreateWithoutProfileInput>
create: Prisma.XOR<Prisma.UserCreateWithoutProfilesInput, Prisma.UserUncheckedCreateWithoutProfilesInput>
}
export type UserUpsertWithoutProfileInput = {
update: Prisma.XOR<Prisma.UserUpdateWithoutProfileInput, Prisma.UserUncheckedUpdateWithoutProfileInput>
create: Prisma.XOR<Prisma.UserCreateWithoutProfileInput, Prisma.UserUncheckedCreateWithoutProfileInput>
export type UserUpsertWithoutProfilesInput = {
update: Prisma.XOR<Prisma.UserUpdateWithoutProfilesInput, Prisma.UserUncheckedUpdateWithoutProfilesInput>
create: Prisma.XOR<Prisma.UserCreateWithoutProfilesInput, Prisma.UserUncheckedCreateWithoutProfilesInput>
where?: Prisma.UserWhereInput
}
export type UserUpdateToOneWithWhereWithoutProfileInput = {
export type UserUpdateToOneWithWhereWithoutProfilesInput = {
where?: Prisma.UserWhereInput
data: Prisma.XOR<Prisma.UserUpdateWithoutProfileInput, Prisma.UserUncheckedUpdateWithoutProfileInput>
data: Prisma.XOR<Prisma.UserUpdateWithoutProfilesInput, Prisma.UserUncheckedUpdateWithoutProfilesInput>
}
export type UserUpdateWithoutProfileInput = {
export type UserUpdateWithoutProfilesInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
@@ -664,7 +673,7 @@ export type UserUpdateWithoutProfileInput = {
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput
}
export type UserUncheckedUpdateWithoutProfileInput = {
export type UserUncheckedUpdateWithoutProfilesInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
@@ -689,7 +698,7 @@ export type UserCreateWithoutSessionsInput = {
updatedAt?: Date | string
role?: Prisma.UserRoleCreateNestedOneWithoutUsersInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileCreateNestedOneWithoutUserInput
profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput
}
@@ -704,7 +713,7 @@ export type UserUncheckedCreateWithoutSessionsInput = {
updatedAt?: Date | string
roleId?: number | null
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileUncheckedCreateNestedOneWithoutUserInput
profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput
}
@@ -735,7 +744,7 @@ export type UserUpdateWithoutSessionsInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.UserRoleUpdateOneWithoutUsersNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUpdateOneWithoutUserNestedInput
profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput
}
@@ -750,7 +759,7 @@ export type UserUncheckedUpdateWithoutSessionsInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
roleId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUncheckedUpdateOneWithoutUserNestedInput
profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput
}
@@ -765,7 +774,7 @@ export type UserCreateWithoutAccountsInput = {
updatedAt?: Date | string
role?: Prisma.UserRoleCreateNestedOneWithoutUsersInput
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileCreateNestedOneWithoutUserInput
profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput
}
@@ -780,7 +789,7 @@ export type UserUncheckedCreateWithoutAccountsInput = {
updatedAt?: Date | string
roleId?: number | null
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileUncheckedCreateNestedOneWithoutUserInput
profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput
}
@@ -811,7 +820,7 @@ export type UserUpdateWithoutAccountsInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.UserRoleUpdateOneWithoutUsersNestedInput
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUpdateOneWithoutUserNestedInput
profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput
}
@@ -826,7 +835,7 @@ export type UserUncheckedUpdateWithoutAccountsInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
roleId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUncheckedUpdateOneWithoutUserNestedInput
profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput
}
@@ -841,7 +850,7 @@ export type UserCreateWithoutRoleInput = {
updatedAt?: Date | string
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileCreateNestedOneWithoutUserInput
profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput
}
@@ -856,7 +865,7 @@ export type UserUncheckedCreateWithoutRoleInput = {
updatedAt?: Date | string
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileUncheckedCreateNestedOneWithoutUserInput
profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput
}
@@ -912,7 +921,7 @@ export type UserCreateWithoutUserProclamationsInput = {
role?: Prisma.UserRoleCreateNestedOneWithoutUsersInput
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileCreateNestedOneWithoutUserInput
profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput
}
@@ -927,7 +936,7 @@ export type UserUncheckedCreateWithoutUserProclamationsInput = {
roleId?: number | null
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileUncheckedCreateNestedOneWithoutUserInput
profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput
}
@@ -958,7 +967,7 @@ export type UserUpdateWithoutUserProclamationsInput = {
role?: Prisma.UserRoleUpdateOneWithoutUsersNestedInput
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUpdateOneWithoutUserNestedInput
profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput
}
@@ -973,7 +982,7 @@ export type UserUncheckedUpdateWithoutUserProclamationsInput = {
roleId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUncheckedUpdateOneWithoutUserNestedInput
profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput
}
@@ -988,7 +997,7 @@ export type UserCreateWithoutLicenseKeysInput = {
role?: Prisma.UserRoleCreateNestedOneWithoutUsersInput
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileCreateNestedOneWithoutUserInput
profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput
}
@@ -1003,7 +1012,7 @@ export type UserUncheckedCreateWithoutLicenseKeysInput = {
roleId?: number | null
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileUncheckedCreateNestedOneWithoutUserInput
profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput
}
@@ -1034,7 +1043,7 @@ export type UserUpdateWithoutLicenseKeysInput = {
role?: Prisma.UserRoleUpdateOneWithoutUsersNestedInput
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUpdateOneWithoutUserNestedInput
profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput
}
@@ -1049,7 +1058,7 @@ export type UserUncheckedUpdateWithoutLicenseKeysInput = {
roleId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUncheckedUpdateOneWithoutUserNestedInput
profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput
}
@@ -1073,7 +1082,7 @@ export type UserUpdateWithoutRoleInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUpdateOneWithoutUserNestedInput
profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput
}
@@ -1088,7 +1097,7 @@ export type UserUncheckedUpdateWithoutRoleInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUncheckedUpdateOneWithoutUserNestedInput
profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput
}
@@ -1111,6 +1120,7 @@ export type UserUncheckedUpdateManyWithoutRoleInput = {
export type UserCountOutputType = {
sessions: number
accounts: number
profiles: number
licenseKeys: number
userProclamations: number
}
@@ -1118,6 +1128,7 @@ export type UserCountOutputType = {
export type UserCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
sessions?: boolean | UserCountOutputTypeCountSessionsArgs
accounts?: boolean | UserCountOutputTypeCountAccountsArgs
profiles?: boolean | UserCountOutputTypeCountProfilesArgs
licenseKeys?: boolean | UserCountOutputTypeCountLicenseKeysArgs
userProclamations?: boolean | UserCountOutputTypeCountUserProclamationsArgs
}
@@ -1146,6 +1157,13 @@ export type UserCountOutputTypeCountAccountsArgs<ExtArgs extends runtime.Types.E
where?: Prisma.AccountWhereInput
}
/**
* UserCountOutputType without action
*/
export type UserCountOutputTypeCountProfilesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.ProfileWhereInput
}
/**
* UserCountOutputType without action
*/
@@ -1173,7 +1191,7 @@ export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
role?: boolean | Prisma.User$roleArgs<ExtArgs>
sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs>
accounts?: boolean | Prisma.User$accountsArgs<ExtArgs>
profile?: boolean | Prisma.User$profileArgs<ExtArgs>
profiles?: boolean | Prisma.User$profilesArgs<ExtArgs>
licenseKeys?: boolean | Prisma.User$licenseKeysArgs<ExtArgs>
userProclamations?: boolean | Prisma.User$userProclamationsArgs<ExtArgs>
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
@@ -1219,7 +1237,7 @@ export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs =
role?: boolean | Prisma.User$roleArgs<ExtArgs>
sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs>
accounts?: boolean | Prisma.User$accountsArgs<ExtArgs>
profile?: boolean | Prisma.User$profileArgs<ExtArgs>
profiles?: boolean | Prisma.User$profilesArgs<ExtArgs>
licenseKeys?: boolean | Prisma.User$licenseKeysArgs<ExtArgs>
userProclamations?: boolean | Prisma.User$userProclamationsArgs<ExtArgs>
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
@@ -1237,7 +1255,7 @@ export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
role: Prisma.$UserRolePayload<ExtArgs> | null
sessions: Prisma.$SessionPayload<ExtArgs>[]
accounts: Prisma.$AccountPayload<ExtArgs>[]
profile: Prisma.$ProfilePayload<ExtArgs> | null
profiles: Prisma.$ProfilePayload<ExtArgs>[]
licenseKeys: Prisma.$LicenseKeyPayload<ExtArgs>[]
userProclamations: Prisma.$UserProclamationsPayload<ExtArgs>[]
}
@@ -1647,7 +1665,7 @@ export interface Prisma__UserClient<T, Null = never, ExtArgs extends runtime.Typ
role<T extends Prisma.User$roleArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$roleArgs<ExtArgs>>): Prisma.Prisma__UserRoleClient<runtime.Types.Result.GetResult<Prisma.$UserRolePayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
sessions<T extends Prisma.User$sessionsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$sessionsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$SessionPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
accounts<T extends Prisma.User$accountsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$accountsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$AccountPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
profile<T extends Prisma.User$profileArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$profileArgs<ExtArgs>>): Prisma.Prisma__ProfileClient<runtime.Types.Result.GetResult<Prisma.$ProfilePayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
profiles<T extends Prisma.User$profilesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$profilesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ProfilePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
licenseKeys<T extends Prisma.User$licenseKeysArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$licenseKeysArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$LicenseKeyPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
userProclamations<T extends Prisma.User$userProclamationsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$userProclamationsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$UserProclamationsPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
/**
@@ -2155,9 +2173,9 @@ export type User$accountsArgs<ExtArgs extends runtime.Types.Extensions.InternalA
}
/**
* User.profile
* User.profiles
*/
export type User$profileArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
export type User$profilesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Profile
*/
@@ -2171,6 +2189,11 @@ export type User$profileArgs<ExtArgs extends runtime.Types.Extensions.InternalAr
*/
include?: Prisma.ProfileInclude<ExtArgs> | null
where?: Prisma.ProfileWhereInput
orderBy?: Prisma.ProfileOrderByWithRelationInput | Prisma.ProfileOrderByWithRelationInput[]
cursor?: Prisma.ProfileWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.ProfileScalarFieldEnum | Prisma.ProfileScalarFieldEnum[]
}
/**
+23
View File
@@ -0,0 +1,23 @@
import { AsyncLocalStorage } from 'node:async_hooks';
/**
* The actor for the current request, used by the Prisma audit extension to
* attribute database mutations. Populated in `hooks.server.ts`.
*/
export type AuditContext = {
userId: string | null;
email: string | null;
roleId: number | null;
page: string;
ip: string | null;
};
const storage = new AsyncLocalStorage<AuditContext>();
export function runWithAuditContext<T>(context: AuditContext, fn: () => T): T {
return storage.run(context, fn);
}
export function getAuditContext(): AuditContext | undefined {
return storage.getStore();
}
+78
View File
@@ -0,0 +1,78 @@
import { describe, it, expect } from 'vitest';
import { describeAction, entityIdFor, sanitizeAuditValue, shouldAudit } from './audit';
describe('shouldAudit', () => {
it('audits mutations', () => {
expect(shouldAudit('User', 'create')).toBe(true);
expect(shouldAudit('Profile', 'update')).toBe(true);
expect(shouldAudit('Proclamation', 'delete')).toBe(true);
expect(shouldAudit('LicenseKey', 'updateMany')).toBe(true);
});
it('ignores reads', () => {
expect(shouldAudit('User', 'findMany')).toBe(false);
expect(shouldAudit('User', 'findUnique')).toBe(false);
expect(shouldAudit('User', 'count')).toBe(false);
});
it('ignores the audit log and auth internals', () => {
for (const model of ['AuditLog', 'Session', 'Account', 'Verification']) {
expect(shouldAudit(model, 'create')).toBe(false);
}
});
});
describe('sanitizeAuditValue', () => {
it('redacts sensitive keys', () => {
const out = sanitizeAuditValue({
password: 'hunter2',
token: 'abc',
keyHash: 'deadbeef',
email: 'jane@example.com'
}) as Record<string, unknown>;
expect(out.password).toBe('[redacted]');
expect(out.token).toBe('[redacted]');
expect(out.keyHash).toBe('[redacted]');
expect(out.email).toBe('jane@example.com');
});
it('converts dates to ISO strings', () => {
const date = new Date('2026-01-01T00:00:00.000Z');
expect(sanitizeAuditValue({ at: date })).toEqual({ at: '2026-01-01T00:00:00.000Z' });
});
it('handles arrays and nullish values', () => {
expect(sanitizeAuditValue([1, null, 'a'])).toEqual([1, null, 'a']);
expect(sanitizeAuditValue(null)).toBeNull();
expect(sanitizeAuditValue(undefined)).toBeNull();
});
it('truncates very long strings', () => {
const long = 'a'.repeat(21000);
const out = sanitizeAuditValue(long) as string;
expect(out.length).toBe(20001);
expect(out.endsWith('…')).toBe(true);
});
});
describe('entityIdFor', () => {
it('prefers the result id', () => {
expect(entityIdFor({ where: { id: 'w' } }, { id: 'r' }, { id: 'b' })).toBe('r');
});
it('falls back to where, data, then before', () => {
expect(entityIdFor({ where: { id: 5 } }, null, null)).toBe('5');
expect(entityIdFor({ data: { id: 'd' } }, null, null)).toBe('d');
expect(entityIdFor({}, null, { id: 'b' })).toBe('b');
expect(entityIdFor({}, null, null)).toBeNull();
});
});
describe('describeAction', () => {
it('labels bulk operations', () => {
expect(describeAction('updateMany')).toBe('bulk update');
expect(describeAction('deleteMany')).toBe('bulk delete');
expect(describeAction('create')).toBe('create');
});
});
+87
View File
@@ -0,0 +1,87 @@
/**
* Pure helpers for the audit extension. Kept free of database imports so the
* extension in `$lib/db.ts` can use them without a cycle.
*/
/** Prisma operations that change data. Reads are never audited. */
const MUTATION_OPERATIONS = new Set([
'create',
'createMany',
'createManyAndReturn',
'update',
'updateMany',
'updateManyAndReturn',
'upsert',
'delete',
'deleteMany'
]);
/**
* Models we never audit:
* - `AuditLog` itself (would recurse and is not user data)
* - `Session`/`Account`/`Verification` are high-churn auth internals that hold
* tokens, password hashes and other secrets. Business mutations are audited.
*/
const EXCLUDED_MODELS = new Set(['AuditLog', 'Session', 'Account', 'Verification']);
export function shouldAudit(model: string, operation: string): boolean {
return !EXCLUDED_MODELS.has(model) && MUTATION_OPERATIONS.has(operation);
}
const SENSITIVE_KEY = /pass(word)?|secret|token|hash/i;
/**
* Make a value safe to persist as JSON: dates become ISO strings, sensitive
* keys are redacted, very long strings are truncated, and cycles/depth are
* bounded.
*/
export function sanitizeAuditValue(value: unknown, depth = 0): unknown {
if (value === undefined || value === null) return null;
if (depth > 8) return '[truncated]';
if (value instanceof Date) return value.toISOString();
if (Array.isArray(value)) return value.map((item) => sanitizeAuditValue(item, depth + 1));
if (typeof value === 'object') {
const out: Record<string, unknown> = {};
for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {
out[key] = SENSITIVE_KEY.test(key) ? '[redacted]' : sanitizeAuditValue(nested, depth + 1);
}
return out;
}
if (typeof value === 'string' && value.length > 20000) return `${value.slice(0, 20000)}…`;
return value;
}
/** Best-effort row id for the changed record. */
export function entityIdFor(args: unknown, result: unknown, before: unknown): string | null {
const where = (args as { where?: { id?: unknown } } | null)?.where;
const data = (args as { data?: { id?: unknown } } | null)?.data;
const resultId = (result as { id?: unknown } | null)?.id;
const first = Array.isArray(before) ? before[0] : before;
const beforeId = (first as { id?: unknown } | undefined)?.id;
const id = resultId ?? where?.id ?? data?.id ?? beforeId;
return id === undefined || id === null ? null : String(id);
}
/** Human label for an operation, e.g. `updateMany` → `bulk update`. */
export function describeAction(operation: string): string {
switch (operation) {
case 'create':
return 'create';
case 'update':
return 'update';
case 'delete':
return 'delete';
case 'upsert':
return 'upsert';
case 'createMany':
case 'createManyAndReturn':
return 'bulk create';
case 'updateMany':
case 'updateManyAndReturn':
return 'bulk update';
case 'deleteMany':
return 'bulk delete';
default:
return operation;
}
}
+26
View File
@@ -0,0 +1,26 @@
import { env } from '$env/dynamic/private';
import { sendMail } from './resend';
/**
* Send the "finish your registration" email to a user created by an admin.
*
* The link points at Better Auth's reset-password callback, which redirects to
* `/reset-password?invite=1&token=...`. There the user sets a password, then
* logs in and is sent to `/profile/setup` to complete their profile.
*
* Uses a Resend-hosted template (`registration-email` by default, or
* `RESEND_REGISTRATION_TEMPLATE_ID`) with variables:
* - `target_email_address`
* - `registration_link`
*/
export async function sendRegistrationEmail(options: { email: string; url: string }) {
await sendMail({
to: options.email,
subject: 'Welcome to Radiant Wave — set your password',
templateId: env.RESEND_REGISTRATION_TEMPLATE_ID || 'registration-email',
data: {
target_email_address: options.email,
registration_link: options.url
}
});
}
+18
View File
@@ -0,0 +1,18 @@
import { auth } from '$lib/auth';
/**
* Email an invited user a link to set their password and finish their profile.
*
* Reuses Better Auth's password-reset token flow; the `invite=1` marker makes
* the reset-password hook send the `registration-email` template instead of the
* usual "you requested a reset" email. The link lands on /reset-password, and
* after setting a password the user logs in and is sent to /profile/setup.
*/
export async function sendRegistrationInvite(email: string): Promise<void> {
await auth.api.requestPasswordReset({
body: {
email,
redirectTo: '/reset-password?invite=1'
}
});
}
+3 -3
View File
@@ -26,7 +26,7 @@ function getResend(): Resend {
* @example
* await sendMail({
* to: 'jane@example.com',
* templateId: 'tmpl_abc123', // optional — defaults to RESEND_TEMPLATE_ID
* templateId: 'tmpl_abc123', // required — Resend template id or alias
* subject: 'Your license key is ready', // optional — defaults to 'Template Email'
* data: { // optional — template variables
* firstName: 'Jane',
@@ -40,7 +40,7 @@ function getResend(): Resend {
export async function sendMail(options: {
to: string;
subject?: string;
templateId?: string;
templateId: string;
data?: Record<string, string | number>;
}) {
const { to, subject, templateId, data } = options;
@@ -50,7 +50,7 @@ export async function sendMail(options: {
to: [to],
subject: subject ?? 'Template Email',
template: {
id: templateId ?? env.RESEND_TEMPLATE_ID,
id: templateId,
variables: data ?? {}
}
});
+28
View File
@@ -0,0 +1,28 @@
/**
* Role hierarchy. Lower ids are more privileged:
* SUPER = 1, ADMIN = 2, STAFF = 3, USER = 4
*
* `roleId` is exposed on the Better Auth session user (see `auth.ts`
* `user.additionalFields`), so these helpers never need a database call.
*/
export const SUPER = 1;
export const ADMIN = 2;
export const STAFF = 3;
export const USER = 4;
/** The current user's role id, defaulting to the least privileged role. */
export function roleId(locals: App.Locals): number {
return locals.user?.roleId ?? USER;
}
export function isSuper(locals: App.Locals): boolean {
return roleId(locals) <= SUPER;
}
export function isAdmin(locals: App.Locals): boolean {
return roleId(locals) <= ADMIN;
}
export function isStaff(locals: App.Locals): boolean {
return roleId(locals) <= STAFF;
}
+22 -47
View File
@@ -5,9 +5,9 @@ import {
canonicalIdForFrequency,
serializeCategory,
serializeColorPalette,
serializeGroup,
serializeLicense,
serializeProclamation,
serializeSubscriptionLevel
serializeProclamation
} from './v2-serializers';
describe('canonicalIdForFrequency', () => {
@@ -61,7 +61,7 @@ describe('serializeProclamation', () => {
name: 'Peace & Relaxation',
content: '<p>The text content, HTML formatted</p>',
categoryId: 5,
subscriptionLevelId: 1
groupId: 1
};
it('derives solfeggioFrequencyId from the Hz value, not the DB id', () => {
@@ -84,13 +84,13 @@ describe('serializeProclamation', () => {
expect(record).toBeNull();
});
it('passes through a null subscriptionLevelId (kiosk treats as level 1)', () => {
it('passes through categoryId and groupId unchanged', () => {
const record = serializeProclamation({
...base,
subscriptionLevelId: null,
solfeggioFrequency: solarPlexus528
});
expect(record?.subscriptionLevelId).toBeNull();
expect(record?.categoryId).toBe(5);
expect(record?.groupId).toBe(1);
});
it('embeds a 7-color palette when provided', () => {
@@ -116,64 +116,39 @@ describe('serializeProclamation', () => {
});
describe('serializeCategory', () => {
it('keeps parentId null for root categories', () => {
expect(serializeCategory({ id: 4, name: 'Spiritual', sortOrder: 0, parentId: null })).toEqual({
it('maps rows to a flat {id, name, sortOrder} with no parentId', () => {
expect(serializeCategory({ id: 4, name: 'Spiritual', sortOrder: 0 })).toEqual({
id: 4,
name: 'Spiritual',
sortOrder: 0,
parentId: null
sortOrder: 0
});
expect(
Object.keys(serializeCategory({ id: 4, name: 'Spiritual', sortOrder: 0 }))
).not.toContain('parentId');
});
});
it('keeps parentId int for children', () => {
expect(serializeCategory({ id: 5, name: 'General', sortOrder: 0, parentId: 4 })).toEqual({
id: 5,
describe('serializeGroup', () => {
it('maps rows to {id, name, sortOrder}', () => {
expect(serializeGroup({ id: 1, name: 'General', sortOrder: 0 })).toEqual({
id: 1,
name: 'General',
sortOrder: 0,
parentId: 4
sortOrder: 0
});
});
});
describe('serializeLicense', () => {
it('returns subscription_level as an object when the level row exists', () => {
it('returns only key and email_address (levels removed)', () => {
const result = serializeLicense({
key: 'XXXX-XXXX-XXXX-XXXX',
emailAddress: 'user@example.com',
levelName: 'Advanced',
level: { id: 2, name: 'Advanced', description: 'General + Specific' }
emailAddress: 'user@example.com'
});
expect(result).toEqual({
key: 'XXXX-XXXX-XXXX-XXXX',
email_address: 'user@example.com',
subscription_level: { id: 2, name: 'Advanced', description: 'General + Specific' }
});
});
it('falls back to a structurally consistent object when no level row matches', () => {
const result = serializeLicense({
key: 'XXXX-XXXX-XXXX-XXXX',
emailAddress: 'user@example.com',
levelName: 'SomeCustomLevel',
level: null
});
expect(result).toEqual({
key: 'XXXX-XXXX-XXXX-XXXX',
email_address: 'user@example.com',
subscription_level: { id: null, name: 'SomeCustomLevel', description: null }
});
});
});
describe('serializeSubscriptionLevel', () => {
it('maps rows to {id, name, description}', () => {
expect(
serializeSubscriptionLevel({ id: 1, name: 'Basic', description: 'General only' })
).toEqual({
id: 1,
name: 'Basic',
description: 'General only'
email_address: 'user@example.com'
});
expect(Object.keys(result)).not.toContain('subscription_level');
});
});
+14 -35
View File
@@ -55,8 +55,8 @@ export type V2Proclamation = {
name: string;
content: string;
categoryId: number;
groupId: number;
solfeggioFrequencyId: number;
subscriptionLevelId: number | null;
colorPalette?: string[];
};
@@ -65,7 +65,7 @@ export type ProclamationWithFrequency = {
name: string;
content: string;
categoryId: number;
subscriptionLevelId: number | null;
groupId: number;
solfeggioFrequency: { value: number };
};
@@ -83,8 +83,8 @@ export function serializeProclamation(
name: p.name,
content: p.content,
categoryId: p.categoryId,
solfeggioFrequencyId,
subscriptionLevelId: p.subscriptionLevelId
groupId: p.groupId,
solfeggioFrequencyId
};
if (palette && palette.length > 0) {
@@ -98,53 +98,32 @@ export type V2Category = {
id: number;
name: string;
sortOrder: number;
parentId: number | null;
};
export function serializeCategory(c: {
export function serializeCategory(c: { id: number; name: string; sortOrder: number }): V2Category {
return { id: c.id, name: c.name, sortOrder: c.sortOrder };
}
export type V2Group = {
id: number;
name: string;
sortOrder: number;
parentId: number | null;
}): V2Category {
return { id: c.id, name: c.name, sortOrder: c.sortOrder, parentId: c.parentId };
}
export type V2SubscriptionLevel = {
id: number;
name: string;
description: string | null;
};
export function serializeSubscriptionLevel(level: {
id: number;
name: string;
description: string | null;
}): V2SubscriptionLevel {
return { id: level.id, name: level.name, description: level.description };
export function serializeGroup(g: { id: number; name: string; sortOrder: number }): V2Group {
return { id: g.id, name: g.name, sortOrder: g.sortOrder };
}
export type V2License = {
key: string;
email_address: string;
subscription_level: { id: number | null; name: string; description: string | null };
};
/** Build the POST /license response. `subscription_level` is always an object;
* when no SubscriptionLevel row matches the license's level name, fall back to
* a structurally consistent object with a null id/description. */
export function serializeLicense(opts: {
key: string;
emailAddress: string;
levelName: string;
level: { id: number; name: string; description: string | null } | null;
}): V2License {
/** Build the POST /license response. */
export function serializeLicense(opts: { key: string; emailAddress: string }): V2License {
return {
key: opts.key,
email_address: opts.emailAddress,
subscription_level: opts.level
? { id: opts.level.id, name: opts.level.name, description: opts.level.description }
: { id: null, name: opts.levelName, description: null }
email_address: opts.emailAddress
};
}
+1 -1
View File
@@ -17,7 +17,7 @@ function signedRequest(opts: {
const path = '/api/v2/automations/create/account';
const timestamp = String(Math.floor((opts.now ?? new Date()).getTime() / 1000));
const nonce = 'a48c0f2e-5f34-4b0b-9a12-3c7d6b8e1f00';
const body = '{"email":"user@example.com","subscription_level":"Basic"}';
const body = '{"email":"user@example.com"}';
let ts = timestamp;
let nonceValue = nonce;
+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 }>;
};
+16 -4
View File
@@ -51,6 +51,7 @@ export function mockUserWithProfile(overrides?: Record<string, unknown>) {
lastName: 'Doe',
phoneNumber: '555-0100'
},
profiles: [{ id: 'profile-1', userId: 'user-1' }],
...overrides
});
}
@@ -76,6 +77,7 @@ export function mockUserWithProfileAndAddresses(overrides?: Record<string, unkno
}
]
},
profiles: [{ id: 'profile-1', userId: 'user-1' }],
...overrides
});
}
@@ -86,6 +88,8 @@ export function mockLicenseKey(overrides?: Record<string, unknown>) {
id: 'key-1',
key: 'AAAA-BBBB-CCCC-DDDD',
active: true,
createdAt: new Date('2026-01-01T00:00:00.000Z'),
lastValidatedAt: null,
level: 'standard',
userId: 'user-1',
...overrides
@@ -100,6 +104,7 @@ export function mockProclamation(overrides?: Record<string, unknown>) {
content: '<p>Test content</p>',
solfeggioFrequencyId: 1,
categoryId: 1,
groupId: 1,
price: 0.99,
solfeggioFrequency: {
id: 1,
@@ -108,9 +113,7 @@ export function mockProclamation(overrides?: Record<string, unknown>) {
},
category: {
id: 1,
name: 'Healing',
parentId: null,
parent: null
name: 'Healing'
},
...overrides
};
@@ -121,7 +124,16 @@ export function mockCategory(overrides?: Record<string, unknown>) {
return {
id: 1,
name: 'Healing',
parentId: null,
...overrides
};
}
/** Minimal proclamation group. */
export function mockGroup(overrides?: Record<string, unknown>) {
return {
id: 1,
name: 'General',
sortOrder: 0,
...overrides
};
}
@@ -1,96 +0,0 @@
import type { PageServerLoad } from './$types';
import { prisma } from '$lib/db';
import { error, fail, type Actions } from '@sveltejs/kit';
import { serverFormatPhoneNumber } from '$lib/utils';
export const load: PageServerLoad = async ({ locals }) => {
if (!locals.user?.id) {
throw error(403, 'not authorized');
}
const user = await prisma.user.findUnique({
where: { id: locals.user.id },
include: {
profile: {
include: { addresses: true }
}
}
});
if (!user) {
throw error(403, 'not authorized');
}
return { user };
};
export const actions: Actions = {
updateProfile: async ({ request, locals }) => {
if (!locals.user?.id) {
throw error(403, 'not authorized');
}
const data = await request.formData();
const firstName = data.get('firstName') as string;
const lastName = data.get('lastName') as string;
const phoneNumber = serverFormatPhoneNumber(data.get('phoneNumber') as string);
if (!firstName || !lastName || !phoneNumber) {
return fail(400, { error: 'All fields are required.', action: 'updateProfile' });
}
try {
await prisma.profile.update({
where: { userId: locals.user.id },
data: { firstName, lastName, phoneNumber }
});
return { success: true, action: 'updateProfile' };
} catch (err) {
console.error('Profile update error:', err);
return fail(500, { error: 'Failed to update profile.', action: 'updateProfile' });
}
},
addAddress: async ({ request, locals }) => {
if (!locals.user?.id) {
throw error(403, 'not authorized');
}
const data = await request.formData();
const street = data.get('street') as string;
const city = data.get('city') as string;
const state = data.get('state') as string;
const postalCode = data.get('postalCode') as string;
const country = data.get('country') as string;
if (!street || !city || !state || !postalCode || !country) {
return fail(400, { error: 'All address fields are required.', action: 'addAddress' });
}
const profile = await prisma.profile.findUnique({
where: { userId: locals.user.id },
select: { id: true }
});
if (!profile) {
throw error(400, 'Profile not found');
}
try {
await prisma.address.create({
data: {
profileId: profile.id,
street,
city,
state,
postalCode,
country
}
});
return { success: true, action: 'addAddress' };
} catch (err) {
console.error('Address create error:', err);
return fail(500, { error: 'Failed to add address.', action: 'addAddress' });
}
}
};
@@ -1,243 +0,0 @@
<script lang="ts">
import { clientFormatPhoneNumber } from '$lib/utils';
import type { PageProps } from './$types';
let { data, form }: PageProps = $props();
let initialPhone = $derived(clientFormatPhoneNumber(data.user.profile?.phoneNumber ?? ''));
let phoneNumber = $state('');
let phoneInitialized = $state(false);
$effect(() => {
if (!phoneInitialized) {
phoneNumber = initialPhone;
phoneInitialized = true;
}
});
let showAddressForm = $state(false);
function handlePhoneInput(event: Event) {
const input = event.target as HTMLInputElement;
phoneNumber = clientFormatPhoneNumber(input.value);
}
</script>
<div class="flex h-full min-h-0 flex-col gap-8 p-6 dark:text-gray-100">
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">Account</h1>
<!-- Email (read-only) -->
<div>
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">Email</p>
<p class="mt-1 text-gray-900 dark:text-gray-100">{data.user.email}</p>
</div>
<!-- Profile Form -->
<section>
<h2 class="mb-4 text-lg font-medium text-gray-900 dark:text-gray-100">Profile</h2>
{#if form?.action === 'updateProfile' && form?.success}
<div
class="mb-4 rounded-lg border border-green-200 bg-green-50 p-3 text-green-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
Profile updated successfully.
</div>
{/if}
{#if form?.action === 'updateProfile' && form?.error}
<div
class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
{form.error}
</div>
{/if}
<form method="POST" action="?/updateProfile" class="flex max-w-lg flex-col gap-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label for="firstName" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
First Name
</label>
<input
type="text"
id="firstName"
name="firstName"
value={data.user.profile?.firstName ?? ''}
required
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
/>
</div>
<div>
<label for="lastName" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Last Name
</label>
<input
type="text"
id="lastName"
name="lastName"
value={data.user.profile?.lastName ?? ''}
required
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
/>
</div>
</div>
<div>
<label for="phoneNumber" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Phone Number
</label>
<input
type="tel"
id="phoneNumber"
name="phoneNumber"
bind:value={phoneNumber}
oninput={handlePhoneInput}
required
class="mt-1 w-full max-w-xs rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
/>
</div>
<div>
<button
type="submit"
class="rounded-lg bg-blue-600 px-4 py-2 font-medium text-white transition-colors hover:bg-blue-700 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600"
>
Save Profile
</button>
</div>
</form>
</section>
<!-- Addresses -->
<section>
<div class="mb-4 flex items-center justify-between">
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">Addresses</h2>
<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)}
>
{showAddressForm ? 'Cancel' : 'Add Address'}
</button>
</div>
{#if form?.action === 'addAddress' && form?.success}
<div
class="mb-4 rounded-lg border border-green-200 bg-green-50 p-3 text-green-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
Address added successfully.
</div>
{/if}
{#if form?.action === 'addAddress' && form?.error}
<div
class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
{form.error}
</div>
{/if}
{#if showAddressForm}
<form
method="POST"
action="?/addAddress"
class="mb-6 max-w-lg rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800"
>
<div class="flex flex-col gap-3">
<div>
<label for="street" class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>Street</label
>
<input
type="text"
id="street"
name="street"
required
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
/>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label for="city" class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>City</label
>
<input
type="text"
id="city"
name="city"
required
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
/>
</div>
<div>
<label for="state" class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>State</label
>
<input
type="text"
id="state"
name="state"
required
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
/>
</div>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label
for="postalCode"
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Postal Code
</label>
<input
type="text"
id="postalCode"
name="postalCode"
required
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
/>
</div>
<div>
<label
for="country"
class="block text-sm font-medium text-gray-700 dark:text-gray-300">Country</label
>
<input
type="text"
id="country"
name="country"
required
value="US"
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
/>
</div>
</div>
<div>
<button
type="submit"
class="rounded-lg bg-blue-600 px-4 py-2 font-medium text-white transition-colors hover:bg-blue-700 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600"
>
Save Address
</button>
</div>
</div>
</form>
{/if}
{#if data.user.profile?.addresses && data.user.profile.addresses.length > 0}
<div class="flex flex-col gap-3">
{#each data.user.profile.addresses as address}
<div
class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-900"
>
<p class="text-sm text-gray-900 dark:text-gray-100">{address.street}</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
{address.city}, {address.state}
{address.postalCode}
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">{address.country}</p>
</div>
{/each}
</div>
{:else if !showAddressForm}
<p class="text-sm text-gray-500 dark:text-gray-400">No addresses on file.</p>
{/if}
</section>
</div>
@@ -1,82 +0,0 @@
<!--
There should be a form in the middle
rounded corners
floats above the screen slightly
Profile picture in the center at the top, about 1/3-1/2 the width of the form
Their email address, not editable, instead is a regular sized black text (or white in dark mode)
Input field for their Phone Number
Two inputs, side by side, a little margin between them. First and Last name
A submit button
Save the form then redirect them to their dashboard
-->
<script lang="ts">
import Window from '$lib/components/containers/Window.svelte';
import { clientFormatPhoneNumber } from '$lib/utils';
import type { PageProps } from './$types';
let { data }: PageProps = $props();
let firstName = $state('');
let lastName = $state('');
function splitName(fullName: string | null | undefined): { first: string; last: string } {
if (!fullName) return { first: '', last: '' };
const parts = fullName.trim().split(/\s+/);
return { first: parts[0] ?? '', last: parts.slice(1).join(' ') };
}
$effect(() => {
if (data.user?.name && !firstName) {
const names = splitName(data.user.name);
firstName = names.first;
lastName = names.last;
}
});
let phoneNumber = $state('');
function handlePhoneInput(event: Event) {
const input = event.target as HTMLInputElement;
phoneNumber = clientFormatPhoneNumber(input.value);
}
</script>
<Window>
<form method="POST" class="border p-6 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100">
<p class="mx-auto my-4 text-center italic">Please finish setting up your profile</p>
{#if data.user?.image}
<img src={data.user?.image} class="mx-auto w-[8rem] rounded-full" alt="Profile" />
{:else}
<div class="mx-auto aspect-square w-[8rem] rounded-full bg-slate-900"></div>
{/if}
<p class="mx-auto my-4 text-center">{data.user?.email}</p>
<div class="flex flex-col">
<input type="text" name="userId" value={data.user?.id} class="hidden" />
<input
type="text"
name="firstName"
placeholder="First Name"
class="my-2"
bind:value={firstName}
/>
<input
type="text"
name="lastName"
placeholder="Last Name"
class="my-2"
bind:value={lastName}
/>
<input
type="tel"
name="phoneNumber"
placeholder="Phone Number"
class="my-2"
bind:value={phoneNumber}
oninput={handlePhoneInput}
/>
</div>
<button type="submit" class="bg-blue-300 p-2 px-4 dark:bg-gray-700 dark:text-gray-100"
>Submit</button
>
</form>
</Window>
@@ -7,27 +7,10 @@ export const load: PageServerLoad = async ({ locals }) => {
throw error(403, 'not authorized');
}
const profile = await prisma.profile.findUnique({
const licenseKeys = await prisma.licenseKey.findMany({
where: { userId: locals.user.id },
select: { firstName: true }
orderBy: { createdAt: 'desc' }
});
const [keyStats, proclamationCount] = await Promise.all([
prisma.licenseKey.groupBy({
by: ['active'],
where: { userId: locals.user.id },
_count: true
}),
prisma.proclamation.count()
]);
const activeKeys = keyStats.find((s) => s.active)?._count ?? 0;
const totalKeys = keyStats.reduce((sum, s) => sum + s._count, 0);
return {
firstName: profile?.firstName ?? 'User',
activeKeys,
totalKeys,
proclamationCount
};
return { licenseKeys };
};
@@ -1,42 +1,127 @@
<script lang="ts">
import DashboardSection from '$lib/components/DashboardSection.svelte';
import type { PageProps } from './$types';
let { data }: PageProps = $props();
function formatDate(value: Date | string): string {
const date = value instanceof Date ? value : new Date(value);
return date.toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}
</script>
<div class="flex h-full min-h-0 flex-col gap-6 p-6 dark:text-gray-100">
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">
Welcome, {data.firstName}!
</h1>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<a
href="/license-keys"
class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-900"
>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Active License Keys</p>
<p class="mt-2 text-3xl font-bold text-gray-900 dark:text-gray-100">{data.activeKeys}</p>
<p class="mt-1 text-sm text-gray-400 dark:text-gray-500">{data.totalKeys} total</p>
</a>
<a
href="/admin/proclamations"
class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-900"
>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Proclamations</p>
<p class="mt-2 text-3xl font-bold text-gray-900 dark:text-gray-100">
{data.proclamationCount}
</p>
<p class="mt-1 text-sm text-gray-400 dark:text-gray-500">Browse all</p>
</a>
<a
href="/account"
class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-900"
>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Account</p>
<p class="mt-2 text-lg font-semibold text-gray-900 dark:text-gray-100">Manage Profile</p>
<p class="mt-1 text-sm text-gray-400 dark:text-gray-500">Edit your details</p>
</a>
<div>
<div>
<h1 class="text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl dark:text-slate-100">
Dashboard
</h1>
<p class="mt-1 text-sm text-slate-500 sm:text-base dark:text-slate-400">
Manage your account and license keys.
</p>
</div>
<!--
Dashboard sections. This page is the account management surface; personal
information (name, email, phone, addresses) lives on the Profile page.
Planned sections to add below as <DashboardSection> blocks:
- Schedules: create and manage listening schedules.
- Devices / playback.
- Billing / orders.
-->
<DashboardSection title="License Keys" description="Your active and inactive license keys.">
{#if data.licenseKeys.length > 0}
<div
class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
>
<!-- Desktop / tablet: table -->
<table class="hidden w-full text-left text-sm sm:table">
<thead
class="border-b border-slate-200 bg-slate-50 text-xs tracking-wide text-slate-500 uppercase dark:border-slate-800 dark:bg-slate-950 dark:text-slate-400"
>
<tr>
<th class="px-6 py-3 font-semibold">Key</th>
<th class="px-6 py-3 font-semibold">Status</th>
<th class="px-6 py-3 font-semibold">Created</th>
<th class="px-6 py-3 font-semibold">Last verified</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
{#each data.licenseKeys as key (key.key)}
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800">
<td class="px-6 py-4 font-mono text-slate-800 dark:text-slate-200">{key.key}</td>
<td class="px-6 py-4">
{#if key.active}
<span
class="text-lg text-green-600 dark:text-green-400"
title="Active"
aria-label="Active">☑</span
>
{:else}
<span
class="text-lg text-slate-300 dark:text-slate-600"
title="Inactive"
aria-label="Inactive">☐</span
>
{/if}
</td>
<td class="px-6 py-4 text-slate-600 dark:text-slate-400"
>{formatDate(key.createdAt)}</td
>
<td class="px-6 py-4 text-slate-600 dark:text-slate-400">
{key.lastValidatedAt ? formatDate(key.lastValidatedAt) : 'Never'}
</td>
</tr>
{/each}
</tbody>
</table>
<!-- Mobile: card list -->
<ul class="divide-y divide-slate-100 sm:hidden">
{#each data.licenseKeys as key (key.key)}
<li class="flex flex-col gap-2 px-4 py-4">
<div class="flex items-center justify-between gap-4">
<span class="min-w-0 font-mono text-sm break-all text-slate-800 dark:text-slate-200"
>{key.key}</span
>
<span class="shrink-0">
{#if key.active}
<span
class="text-lg text-green-600 dark:text-green-400"
title="Active"
aria-label="Active">☑</span
>
{:else}
<span
class="text-lg text-slate-300 dark:text-slate-600"
title="Inactive"
aria-label="Inactive">☐</span
>
{/if}
</span>
</div>
<span class="text-xs text-slate-500 dark:text-slate-400"
>Created {formatDate(key.createdAt)}</span
>
<span class="text-xs text-slate-500 dark:text-slate-400">
Last verified {key.lastValidatedAt ? formatDate(key.lastValidatedAt) : 'Never'}
</span>
</li>
{/each}
</ul>
</div>
{:else}
<div
class="rounded-2xl border border-dashed border-slate-300 bg-white p-8 text-center dark:border-slate-700 dark:bg-slate-900"
>
<p class="text-sm text-slate-600 dark:text-slate-400">
You don't have any license keys yet.
</p>
</div>
{/if}
</DashboardSection>
</div>
@@ -1,27 +1,21 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { isHttpError } from '@sveltejs/kit';
import { createLocals, mockUser } from '$lib/test-utils';
import { createLocals, mockUser, mockLicenseKey } from '$lib/test-utils';
vi.mock('$lib/db', () => ({
prisma: {
profile: { findUnique: vi.fn() },
licenseKey: { groupBy: vi.fn() },
proclamation: { count: vi.fn() }
licenseKey: { findMany: vi.fn() }
}
}));
import { prisma } from '$lib/db';
import { load } from './+page.server';
const profileFindUnique = prisma.profile.findUnique 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>;
const licenseKeyFindMany = prisma.licenseKey.findMany as ReturnType<typeof vi.fn>;
describe('(protected)/dashboard page server load', () => {
beforeEach(() => {
profileFindUnique.mockReset();
licenseKeyGroupBy.mockReset();
proclamationCount.mockReset();
licenseKeyFindMany.mockReset();
});
it('throws 403 when locals.user is undefined', async () => {
@@ -31,58 +25,35 @@ describe('(protected)/dashboard page server load', () => {
expect(isHttpError(err, 403)).toBe(true);
});
it('returns firstName from profile', async () => {
profileFindUnique.mockResolvedValue({ firstName: 'Jane' });
licenseKeyGroupBy.mockResolvedValue([]);
proclamationCount.mockResolvedValue(0);
it('returns license keys for the authenticated user', async () => {
const keys = [
mockLicenseKey(),
mockLicenseKey({ id: 'key-2', key: 'EEEE-FFFF-GGGG-HHHH', active: false })
];
licenseKeyFindMany.mockResolvedValue(keys);
const locals = createLocals(mockUser() as any);
const result = (await load({ locals } as any)) as any;
expect(result.firstName).toBe('Jane');
expect(result.licenseKeys).toEqual(keys);
});
it('defaults firstName to "User" when profile is null', async () => {
profileFindUnique.mockResolvedValue(null);
licenseKeyGroupBy.mockResolvedValue([]);
proclamationCount.mockResolvedValue(0);
it('returns empty array when user has no keys', async () => {
licenseKeyFindMany.mockResolvedValue([]);
const locals = createLocals(mockUser() as any);
const result = (await load({ locals } as any)) as any;
expect(result.firstName).toBe('User');
expect(result.licenseKeys).toEqual([]);
});
it('computes activeKeys and totalKeys from groupBy results', async () => {
profileFindUnique.mockResolvedValue({ firstName: 'Jane' });
licenseKeyGroupBy.mockResolvedValue([
{ active: true, _count: 3 },
{ active: false, _count: 2 }
]);
proclamationCount.mockResolvedValue(10);
it('loads license keys filtered by user, newest first', async () => {
licenseKeyFindMany.mockResolvedValue([]);
const locals = createLocals(mockUser() as any);
const result = (await load({ locals } as any)) as any;
expect(result.activeKeys).toBe(3);
expect(result.totalKeys).toBe(5);
});
await load({ locals } as any);
it('returns 0 for activeKeys when none are active', async () => {
profileFindUnique.mockResolvedValue({ firstName: 'Jane' });
licenseKeyGroupBy.mockResolvedValue([{ active: false, _count: 4 }]);
proclamationCount.mockResolvedValue(0);
const locals = createLocals(mockUser() as any);
const result = (await load({ locals } as any)) as any;
expect(result.activeKeys).toBe(0);
expect(result.totalKeys).toBe(4);
});
it('returns proclamationCount', async () => {
profileFindUnique.mockResolvedValue({ firstName: 'Jane' });
licenseKeyGroupBy.mockResolvedValue([]);
proclamationCount.mockResolvedValue(42);
const locals = createLocals(mockUser() as any);
const result = (await load({ locals } as any)) as any;
expect(result.proclamationCount).toBe(42);
expect(licenseKeyFindMany).toHaveBeenCalledWith({
where: { userId: 'user-1' },
orderBy: { createdAt: 'desc' }
});
});
});
@@ -1,16 +0,0 @@
import type { PageServerLoad } from './$types';
import { prisma } from '$lib/db';
import { error } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ locals }) => {
if (!locals.user?.id) {
throw error(403, 'not authorized');
}
const licenseKeys = await prisma.licenseKey.findMany({
where: { userId: locals.user.id },
orderBy: { key: 'asc' }
});
return { licenseKeys };
};
@@ -1,62 +0,0 @@
<script lang="ts">
import type { PageProps } from './$types';
let { data }: PageProps = $props();
</script>
<div class="flex h-full min-h-0 flex-col gap-6 p-6 dark:text-gray-100">
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">License Keys</h1>
{#if data.licenseKeys.length > 0}
<div class="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr>
<th
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase dark:text-gray-400"
>
Key
</th>
<th
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase dark:text-gray-400"
>
Level
</th>
<th
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase dark:text-gray-400"
>
Status
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-900">
{#each data.licenseKeys as key}
<tr>
<td
class="px-6 py-4 font-mono text-sm whitespace-nowrap text-gray-900 dark:text-gray-100"
>
{key.key}
</td>
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-700 dark:text-gray-300">
{key.level}
</td>
<td class="px-6 py-4 text-sm whitespace-nowrap">
<span
class="inline-flex rounded-full px-2 text-xs leading-5 font-semibold {key.active
? 'bg-green-100 text-green-800 dark:bg-gray-700 dark:text-gray-200'
: 'bg-red-100 text-red-800 dark:bg-gray-700 dark:text-gray-200'}"
>
{key.active ? 'Active' : 'Inactive'}
</span>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{:else}
<div class="flex flex-1 items-center justify-center">
<p class="text-lg text-gray-500 dark:text-gray-400">You don't have any license keys yet.</p>
</div>
{/if}
</div>
@@ -1,59 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { isHttpError } from '@sveltejs/kit';
import { createLocals, mockUser, mockLicenseKey } from '$lib/test-utils';
vi.mock('$lib/db', () => ({
prisma: {
licenseKey: { findMany: vi.fn() }
}
}));
import { prisma } from '$lib/db';
import { load } from './+page.server';
const findMany = prisma.licenseKey.findMany as ReturnType<typeof vi.fn>;
describe('(protected)/license-keys page server load', () => {
beforeEach(() => {
findMany.mockReset();
});
it('throws 403 when locals.user is undefined', async () => {
const err = await Promise.resolve(load({ locals: createLocals() } as any)).catch(
(e: unknown) => e
);
expect(isHttpError(err, 403)).toBe(true);
});
it('returns license keys for the authenticated user', async () => {
const keys = [
mockLicenseKey(),
mockLicenseKey({ id: 'key-2', key: 'EEEE-FFFF-GGGG-HHHH', active: false })
];
findMany.mockResolvedValue(keys);
const locals = createLocals(mockUser() as any);
const result = (await load({ locals } as any)) as any;
expect(result.licenseKeys).toEqual(keys);
});
it('passes userId in the where clause', async () => {
findMany.mockResolvedValue([]);
const locals = createLocals(mockUser() as any);
await load({ locals } as any);
expect(findMany).toHaveBeenCalledWith({
where: { userId: 'user-1' },
orderBy: { key: 'asc' }
});
});
it('returns empty array when user has no keys', async () => {
findMany.mockResolvedValue([]);
const locals = createLocals(mockUser() as any);
const result = (await load({ locals } as any)) as any;
expect(result.licenseKeys).toEqual([]);
});
});
@@ -0,0 +1,166 @@
import type { PageServerLoad } from './$types';
import { prisma } from '$lib/db';
import { error, fail, type Actions } from '@sveltejs/kit';
import { serverFormatPhoneNumber } from '$lib/utils';
export const load: PageServerLoad = async ({ locals }) => {
if (!locals.user?.id) {
throw error(403, 'not authorized');
}
const user = await prisma.user.findUnique({ where: { id: locals.user.id } });
if (!user) {
throw error(403, 'not authorized');
}
const profile = await prisma.profile.findFirst({
where: { userId: user.id },
include: { addresses: true }
});
return { user: { ...user, profile } };
};
export const actions: Actions = {
updateProfile: async ({ request, locals }) => {
if (!locals.user?.id) {
throw error(403, 'not authorized');
}
const data = await request.formData();
const firstName = data.get('firstName') as string;
const lastName = data.get('lastName') as string;
const phoneNumber = serverFormatPhoneNumber(data.get('phoneNumber') as string);
if (!firstName || !lastName || !phoneNumber) {
return fail(400, { error: 'All fields are required.', action: 'updateProfile' });
}
try {
await prisma.profile.updateMany({
where: { userId: locals.user.id },
data: { firstName, lastName, phoneNumber }
});
return { success: true, action: 'updateProfile' };
} catch (err) {
console.error('Profile update error:', err);
return fail(500, { error: 'Failed to update profile.', action: 'updateProfile' });
}
},
addAddress: async ({ request, locals }) => {
if (!locals.user?.id) {
throw error(403, 'not authorized');
}
const data = await request.formData();
const street = data.get('street') as string;
const city = data.get('city') as string;
const state = data.get('state') as string;
const postalCode = data.get('postalCode') as string;
const country = data.get('country') as string;
if (!street || !city || !state || !postalCode || !country) {
return fail(400, { error: 'All address fields are required.', action: 'addAddress' });
}
const profile = await prisma.profile.findFirst({
where: { userId: locals.user.id },
select: { id: true }
});
if (!profile) {
throw error(400, 'Profile not found');
}
try {
await prisma.address.create({
data: {
profileId: profile.id,
street,
city,
state,
postalCode,
country
}
});
return { success: true, action: 'addAddress' };
} catch (err) {
console.error('Address create error:', err);
return fail(500, { error: 'Failed to add address.', action: 'addAddress' });
}
},
updateAddress: async ({ request, locals }) => {
if (!locals.user?.id) {
throw error(403, 'not authorized');
}
const data = await request.formData();
const id = data.get('id') as string;
const street = data.get('street') as string;
const city = data.get('city') as string;
const state = data.get('state') as string;
const postalCode = data.get('postalCode') as string;
const country = data.get('country') as string;
if (!id) {
return fail(400, { error: 'Missing address id.', action: 'updateAddress' });
}
if (!street || !city || !state || !postalCode || !country) {
return fail(400, { error: 'All address fields are required.', action: 'updateAddress' });
}
const address = await prisma.address.findFirst({
where: { id, profile: { userId: locals.user.id } },
select: { id: true }
});
if (!address) {
throw error(404, 'Address not found');
}
try {
await prisma.address.update({
where: { id },
data: { street, city, state, postalCode, country }
});
return { success: true, action: 'updateAddress' };
} catch (err) {
console.error('Address update error:', err);
return fail(500, { error: 'Failed to update address.', action: 'updateAddress' });
}
},
deleteAddress: async ({ request, locals }) => {
if (!locals.user?.id) {
throw error(403, 'not authorized');
}
const data = await request.formData();
const id = data.get('id') as string;
if (!id) {
return fail(400, { error: 'Missing address id.', action: 'deleteAddress' });
}
const address = await prisma.address.findFirst({
where: { id, profile: { userId: locals.user.id } },
select: { id: true }
});
if (!address) {
throw error(404, 'Address not found');
}
try {
await prisma.address.delete({ where: { id } });
return { success: true, action: 'deleteAddress' };
} catch (err) {
console.error('Address delete error:', err);
return fail(500, { error: 'Failed to delete address.', action: 'deleteAddress' });
}
}
};
@@ -0,0 +1,337 @@
<script lang="ts">
import { clientFormatPhoneNumber } from '$lib/utils';
import type { PageProps } from './$types';
let { data, form }: PageProps = $props();
let initialPhone = $derived(clientFormatPhoneNumber(data.user.profile?.phoneNumber ?? ''));
let phoneNumber = $state('');
let phoneInitialized = $state(false);
$effect(() => {
if (!phoneInitialized) {
phoneNumber = initialPhone;
phoneInitialized = true;
}
});
let showAddressForm = $state(false);
let editingAddressId = $state<string | null>(null);
let addrStreet = $state('');
let addrCity = $state('');
let addrState = $state('');
let addrPostal = $state('');
let addrCountry = $state('US');
function openAddAddress() {
editingAddressId = null;
addrStreet = '';
addrCity = '';
addrState = '';
addrPostal = '';
addrCountry = 'US';
showAddressForm = true;
}
function openEditAddress(address: {
id: string;
street: string;
city: string;
state: string;
postalCode: string;
country: string;
}) {
editingAddressId = address.id;
addrStreet = address.street;
addrCity = address.city;
addrState = address.state;
addrPostal = address.postalCode;
addrCountry = address.country;
showAddressForm = true;
}
function closeAddressForm() {
showAddressForm = false;
editingAddressId = null;
}
function handlePhoneInput(event: Event) {
const input = event.target as HTMLInputElement;
phoneNumber = clientFormatPhoneNumber(input.value);
}
</script>
<div class="mx-auto w-full max-w-3xl">
<h1 class="text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl dark:text-slate-100">
Profile
</h1>
<p class="mt-1 text-sm text-slate-500 sm:text-base dark:text-slate-400">
Manage your account details and addresses.
</p>
<!-- Email (read-only) -->
<div
class="mt-6 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6 dark:border-slate-800 dark:bg-slate-900"
>
<p class="text-xs tracking-wide text-slate-500 uppercase dark:text-slate-400">Email</p>
<p class="mt-1 text-base break-words text-slate-900 dark:text-slate-100">{data.user.email}</p>
</div>
<!-- Profile Form -->
<section
class="mt-6 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6 dark:border-slate-800 dark:bg-slate-900"
>
<h2 class="text-lg font-semibold text-slate-900 dark:text-slate-100">Profile</h2>
{#if form?.action === 'updateProfile' && form?.success}
<div
class="mt-4 rounded-lg bg-green-50 px-4 py-3 text-sm text-green-800 dark:bg-green-950/50 dark:text-green-300"
role="status"
>
Profile updated successfully.
</div>
{/if}
{#if form?.action === 'updateProfile' && form?.error}
<div
class="mt-4 rounded-lg bg-red-50 px-4 py-3 text-sm text-red-700 dark:bg-red-950/50 dark:text-red-300"
role="alert"
>
{form.error}
</div>
{/if}
<form method="POST" action="?/updateProfile" class="mt-5 flex flex-col gap-4">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="flex flex-col gap-1.5">
<label for="firstName" class="text-sm font-medium text-slate-700 dark:text-slate-300">
First Name
</label>
<input
type="text"
id="firstName"
name="firstName"
value={data.user.profile?.firstName ?? ''}
required
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500 dark:border-slate-700 dark:text-slate-100"
/>
</div>
<div class="flex flex-col gap-1.5">
<label for="lastName" class="text-sm font-medium text-slate-700 dark:text-slate-300">
Last Name
</label>
<input
type="text"
id="lastName"
name="lastName"
value={data.user.profile?.lastName ?? ''}
required
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500 dark:border-slate-700 dark:text-slate-100"
/>
</div>
</div>
<div class="flex flex-col gap-1.5">
<label for="phoneNumber" class="text-sm font-medium text-slate-700 dark:text-slate-300">
Phone Number
</label>
<input
type="tel"
id="phoneNumber"
name="phoneNumber"
bind:value={phoneNumber}
oninput={handlePhoneInput}
required
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500 dark:border-slate-700 dark:text-slate-100"
/>
</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 Profile
</button>
</div>
</form>
</section>
<!-- Addresses -->
<section
class="mt-6 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6 dark:border-slate-800 dark:bg-slate-900"
>
<div class="flex items-center justify-between gap-4">
<h2 class="text-lg font-semibold text-slate-900 dark:text-slate-100">Addresses</h2>
<button
type="button"
onclick={() => (showAddressForm ? closeAddressForm() : openAddAddress())}
class="rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-800"
>
{showAddressForm ? 'Cancel' : 'Add Address'}
</button>
</div>
{#if form?.action === 'addAddress' && form?.success}
<div
class="mt-4 rounded-lg bg-green-50 px-4 py-3 text-sm text-green-800 dark:bg-green-950/50 dark:text-green-300"
role="status"
>
Address added successfully.
</div>
{/if}
{#if form?.action === 'updateAddress' && form?.success}
<div
class="mt-4 rounded-lg bg-green-50 px-4 py-3 text-sm text-green-800 dark:bg-green-950/50 dark:text-green-300"
role="status"
>
Address updated successfully.
</div>
{/if}
{#if form?.action === 'deleteAddress' && form?.success}
<div
class="mt-4 rounded-lg bg-green-50 px-4 py-3 text-sm text-green-800 dark:bg-green-950/50 dark:text-green-300"
role="status"
>
Address deleted successfully.
</div>
{/if}
{#if (form?.action === 'addAddress' || form?.action === 'updateAddress' || form?.action === 'deleteAddress') && form?.error}
<div
class="mt-4 rounded-lg bg-red-50 px-4 py-3 text-sm text-red-700 dark:bg-red-950/50 dark:text-red-300"
role="alert"
>
{form.error}
</div>
{/if}
{#if showAddressForm}
<form
method="POST"
action={editingAddressId ? '?/updateAddress' : '?/addAddress'}
class="mt-5 flex flex-col gap-4"
>
{#if editingAddressId}
<input type="hidden" name="id" value={editingAddressId} />
{/if}
<div class="flex flex-col gap-1.5">
<label for="street" class="text-sm font-medium text-slate-700 dark:text-slate-300"
>Street</label
>
<input
type="text"
id="street"
name="street"
bind:value={addrStreet}
required
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500 dark:border-slate-700 dark:text-slate-100"
/>
</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 dark:text-slate-300"
>City</label
>
<input
type="text"
id="city"
name="city"
bind:value={addrCity}
required
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500 dark:border-slate-700 dark:text-slate-100"
/>
</div>
<div class="flex flex-col gap-1.5">
<label for="state" class="text-sm font-medium text-slate-700 dark:text-slate-300"
>State</label
>
<input
type="text"
id="state"
name="state"
bind:value={addrState}
required
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500 dark:border-slate-700 dark:text-slate-100"
/>
</div>
</div>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="flex flex-col gap-1.5">
<label for="postalCode" class="text-sm font-medium text-slate-700 dark:text-slate-300">
Postal Code
</label>
<input
type="text"
id="postalCode"
name="postalCode"
bind:value={addrPostal}
required
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500 dark:border-slate-700 dark:text-slate-100"
/>
</div>
<div class="flex flex-col gap-1.5">
<label for="country" class="text-sm font-medium text-slate-700 dark:text-slate-300"
>Country</label
>
<input
type="text"
id="country"
name="country"
bind:value={addrCountry}
required
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500 dark:border-slate-700 dark:text-slate-100"
/>
</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"
>
{editingAddressId ? 'Update Address' : 'Save Address'}
</button>
</div>
</form>
{/if}
{#if data.user.profile?.addresses && data.user.profile.addresses.length > 0}
<div class="mt-5 grid grid-cols-1 gap-4 sm:grid-cols-2">
{#each data.user.profile.addresses as address (address.id)}
<div
class="rounded-lg border border-slate-200 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-950"
>
<p class="text-sm font-medium text-slate-900 dark:text-slate-100">{address.street}</p>
<p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
{address.city}, {address.state}
{address.postalCode}
</p>
<p class="text-sm text-slate-600 dark:text-slate-400">{address.country}</p>
<div class="mt-3 flex gap-2">
<button
type="button"
onclick={() => openEditAddress(address)}
class="rounded-md border border-slate-300 bg-white px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-800"
>
Edit
</button>
<form
method="POST"
action="?/deleteAddress"
onsubmit={(event) => {
if (!confirm('Delete this address?')) event.preventDefault();
}}
>
<input type="hidden" name="id" value={address.id} />
<button
type="submit"
class="rounded-md border border-red-300 bg-white px-3 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 dark:border-red-800 dark:bg-slate-900 dark:text-red-400 dark:hover:bg-red-950/50"
>
Delete
</button>
</form>
</div>
</div>
{/each}
</div>
{:else if !showAddressForm}
<p class="mt-5 text-sm text-slate-500 dark:text-slate-400">No addresses on file.</p>
{/if}
</section>
</div>
@@ -10,8 +10,8 @@ import {
vi.mock('$lib/db', () => ({
prisma: {
user: { findUnique: vi.fn() },
profile: { update: vi.fn(), findUnique: vi.fn() },
address: { create: vi.fn() }
profile: { findFirst: vi.fn(), updateMany: vi.fn() },
address: { create: vi.fn(), findFirst: vi.fn(), update: vi.fn(), delete: vi.fn() }
}
}));
@@ -19,19 +19,25 @@ import { prisma } from '$lib/db';
import { load, actions } from './+page.server';
const userFindUnique = prisma.user.findUnique as ReturnType<typeof vi.fn>;
const profileUpdate = prisma.profile.update as ReturnType<typeof vi.fn>;
const profileFindUnique = prisma.profile.findUnique as ReturnType<typeof vi.fn>;
const profileFindFirst = prisma.profile.findFirst as ReturnType<typeof vi.fn>;
const profileUpdateMany = prisma.profile.updateMany as ReturnType<typeof vi.fn>;
const addressCreate = prisma.address.create as ReturnType<typeof vi.fn>;
const addressFindFirst = prisma.address.findFirst as ReturnType<typeof vi.fn>;
const addressUpdate = prisma.address.update as ReturnType<typeof vi.fn>;
const addressDelete = prisma.address.delete as ReturnType<typeof vi.fn>;
const invokeAction = (name: string, request: Request, locals: App.Locals) =>
(actions as any)[name]({ request, locals } as any);
describe('(protected)/account page server', () => {
describe('(protected)/profile page server', () => {
beforeEach(() => {
userFindUnique.mockReset();
profileUpdate.mockReset();
profileFindUnique.mockReset();
profileFindFirst.mockReset();
profileUpdateMany.mockReset();
addressCreate.mockReset();
addressFindFirst.mockReset();
addressUpdate.mockReset();
addressDelete.mockReset();
});
describe('load', () => {
@@ -51,24 +57,26 @@ describe('(protected)/account page server', () => {
});
it('returns user with profile and addresses', async () => {
const user = mockUserWithProfileAndAddresses();
const { profile, ...user } = mockUserWithProfileAndAddresses() as any;
userFindUnique.mockResolvedValue(user);
profileFindFirst.mockResolvedValue(profile);
const locals = createLocals(mockUser() as any);
const result = (await load({ locals } as any)) as any;
expect(result.user).toEqual(user);
expect(result.user).toEqual({ ...user, profile });
expect(result.user.profile.addresses).toHaveLength(1);
});
it('includes addresses in the query', async () => {
userFindUnique.mockResolvedValue(mockUserWithProfileAndAddresses());
it('loads the profile with addresses by user id', async () => {
userFindUnique.mockResolvedValue(mockUser());
profileFindFirst.mockResolvedValue({ id: 'profile-1' });
const locals = createLocals(mockUser() as any);
await load({ locals } as any);
expect(userFindUnique).toHaveBeenCalledWith({
where: { id: 'user-1' },
include: { profile: { include: { addresses: true } } }
expect(profileFindFirst).toHaveBeenCalledWith({
where: { userId: 'user-1' },
include: { addresses: true }
});
});
});
@@ -125,7 +133,7 @@ describe('(protected)/account page server', () => {
});
it('updates profile with server-formatted phone number on success', async () => {
profileUpdate.mockResolvedValue({});
profileUpdateMany.mockResolvedValue({});
const request = createFormRequest({
firstName: 'Jane',
@@ -136,14 +144,14 @@ describe('(protected)/account page server', () => {
const locals = createLocals(mockUser() as any);
const result = await invokeAction('updateProfile', request, locals);
expect(result).toEqual({ success: true, action: 'updateProfile' });
expect(profileUpdate).toHaveBeenCalledWith({
expect(profileUpdateMany).toHaveBeenCalledWith({
where: { userId: 'user-1' },
data: { firstName: 'Jane', lastName: 'Smith', phoneNumber: '8175551234' }
});
});
it('returns fail(500) when prisma update throws', async () => {
profileUpdate.mockRejectedValue(new Error('db error'));
profileUpdateMany.mockRejectedValue(new Error('db error'));
const request = createFormRequest({
firstName: 'Jane',
@@ -190,7 +198,7 @@ describe('(protected)/account page server', () => {
});
it('throws 400 when profile is not found', async () => {
profileFindUnique.mockResolvedValue(null);
profileFindFirst.mockResolvedValue(null);
const request = createFormRequest({
street: '123 Main St',
@@ -208,7 +216,7 @@ describe('(protected)/account page server', () => {
});
it('creates address linked to profile on success', async () => {
profileFindUnique.mockResolvedValue({ id: 'profile-1' });
profileFindFirst.mockResolvedValue({ id: 'profile-1' });
addressCreate.mockResolvedValue({});
const request = createFormRequest({
@@ -235,7 +243,7 @@ describe('(protected)/account page server', () => {
});
it('returns fail(500) when prisma create throws', async () => {
profileFindUnique.mockResolvedValue({ id: 'profile-1' });
profileFindFirst.mockResolvedValue({ id: 'profile-1' });
addressCreate.mockRejectedValue(new Error('db error'));
const request = createFormRequest({
@@ -252,4 +260,164 @@ describe('(protected)/account page server', () => {
expect((result as any).status).toBe(500);
});
});
describe('updateAddress action', () => {
it('throws 403 when locals.user is undefined', async () => {
const request = createFormRequest({
id: 'addr-1',
street: '123 Main St',
city: 'Dallas',
state: 'TX',
postalCode: '75001',
country: 'US'
});
const err = await Promise.resolve(
invokeAction('updateAddress', request, createLocals())
).catch((e: unknown) => e);
expect(isHttpError(err, 403)).toBe(true);
});
it('returns fail(400) when id is missing', async () => {
const request = createFormRequest({
id: '',
street: '123 Main St',
city: 'Dallas',
state: 'TX',
postalCode: '75001',
country: 'US'
});
const locals = createLocals(mockUser() as any);
const result = await invokeAction('updateAddress', request, locals);
expect(isActionFailure(result)).toBe(true);
expect((result as any).data.action).toBe('updateAddress');
});
it('throws 404 when the address is not owned by the user', async () => {
addressFindFirst.mockResolvedValue(null);
const request = createFormRequest({
id: 'addr-1',
street: '123 Main St',
city: 'Dallas',
state: 'TX',
postalCode: '75001',
country: 'US'
});
const locals = createLocals(mockUser() as any);
const err = await Promise.resolve(invokeAction('updateAddress', request, locals)).catch(
(e: unknown) => e
);
expect(isHttpError(err, 404)).toBe(true);
expect(addressFindFirst).toHaveBeenCalledWith({
where: { id: 'addr-1', profile: { userId: 'user-1' } },
select: { id: true }
});
});
it('updates the owned address on success', async () => {
addressFindFirst.mockResolvedValue({ id: 'addr-1' });
addressUpdate.mockResolvedValue({});
const request = createFormRequest({
id: 'addr-1',
street: '456 Oak Ave',
city: 'Austin',
state: 'TX',
postalCode: '73301',
country: 'US'
});
const locals = createLocals(mockUser() as any);
const result = await invokeAction('updateAddress', request, locals);
expect(result).toEqual({ success: true, action: 'updateAddress' });
expect(addressUpdate).toHaveBeenCalledWith({
where: { id: 'addr-1' },
data: {
street: '456 Oak Ave',
city: 'Austin',
state: 'TX',
postalCode: '73301',
country: 'US'
}
});
});
it('returns fail(500) when prisma update throws', async () => {
addressFindFirst.mockResolvedValue({ id: 'addr-1' });
addressUpdate.mockRejectedValue(new Error('db error'));
const request = createFormRequest({
id: 'addr-1',
street: '123 Main St',
city: 'Dallas',
state: 'TX',
postalCode: '75001',
country: 'US'
});
const locals = createLocals(mockUser() as any);
const result = await invokeAction('updateAddress', request, locals);
expect(isActionFailure(result)).toBe(true);
expect((result as any).status).toBe(500);
});
});
describe('deleteAddress action', () => {
it('throws 403 when locals.user is undefined', async () => {
const request = createFormRequest({ id: 'addr-1' });
const err = await Promise.resolve(
invokeAction('deleteAddress', request, createLocals())
).catch((e: unknown) => e);
expect(isHttpError(err, 403)).toBe(true);
});
it('returns fail(400) when id is missing', async () => {
const request = createFormRequest({ id: '' });
const locals = createLocals(mockUser() as any);
const result = await invokeAction('deleteAddress', request, locals);
expect(isActionFailure(result)).toBe(true);
expect((result as any).data.action).toBe('deleteAddress');
});
it('throws 404 when the address is not owned by the user', async () => {
addressFindFirst.mockResolvedValue(null);
const request = createFormRequest({ id: 'addr-1' });
const locals = createLocals(mockUser() as any);
const err = await Promise.resolve(invokeAction('deleteAddress', request, locals)).catch(
(e: unknown) => e
);
expect(isHttpError(err, 404)).toBe(true);
});
it('deletes the owned address on success', async () => {
addressFindFirst.mockResolvedValue({ id: 'addr-1' });
addressDelete.mockResolvedValue({});
const request = createFormRequest({ id: 'addr-1' });
const locals = createLocals(mockUser() as any);
const result = await invokeAction('deleteAddress', request, locals);
expect(result).toEqual({ success: true, action: 'deleteAddress' });
expect(addressDelete).toHaveBeenCalledWith({ where: { id: 'addr-1' } });
});
it('returns fail(500) when prisma delete throws', async () => {
addressFindFirst.mockResolvedValue({ id: 'addr-1' });
addressDelete.mockRejectedValue(new Error('db error'));
const request = createFormRequest({ id: 'addr-1' });
const locals = createLocals(mockUser() as any);
const result = await invokeAction('deleteAddress', request, locals);
expect(isActionFailure(result)).toBe(true);
expect((result as any).status).toBe(500);
});
});
});
@@ -0,0 +1,121 @@
<!--
There should be a form in the middle
rounded corners
floats above the screen slightly
Profile picture in the center at the top, about 1/3-1/2 the width of the form
Their email address, not editable, instead is a regular sized black text (or white in dark mode)
Input field for their Phone Number
Two inputs, side by side, a little margin between them. First and Last name
A submit button
Save the form then redirect them to their dashboard
-->
<script lang="ts">
import Window from '$lib/components/containers/Window.svelte';
import { clientFormatPhoneNumber } from '$lib/utils';
import type { PageProps } from './$types';
let { data }: PageProps = $props();
let firstName = $state('');
let lastName = $state('');
function splitName(fullName: string | null | undefined): { first: string; last: string } {
if (!fullName) return { first: '', last: '' };
const parts = fullName.trim().split(/\s+/);
return { first: parts[0] ?? '', last: parts.slice(1).join(' ') };
}
$effect(() => {
if (data.user?.name && !firstName) {
const names = splitName(data.user.name);
firstName = names.first;
lastName = names.last;
}
});
let phoneNumber = $state('');
function handlePhoneInput(event: Event) {
const input = event.target as HTMLInputElement;
phoneNumber = clientFormatPhoneNumber(input.value);
}
</script>
<Window>
<form method="POST" class="flex flex-col items-center gap-5">
<h1 class="text-center text-2xl font-bold text-slate-900 dark:text-slate-100">
Please finish setting up your profile
</h1>
{#if data.user?.image}
<img
src={data.user?.image}
alt="Profile"
class="h-24 w-24 rounded-full object-cover shadow-sm sm:h-28 sm:w-28"
/>
{:else}
<div
class="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 dark:bg-indigo-900 dark:text-indigo-300"
>
{data.user?.name?.charAt(0)?.toUpperCase() ?? '?'}
</div>
{/if}
<p class="text-base text-slate-900 dark:text-slate-100">{data.user?.email}</p>
<div class="flex w-full flex-col gap-4">
<input type="hidden" name="userId" value={data.user?.id} />
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="flex flex-col gap-1.5">
<label for="firstName" class="text-sm font-medium text-slate-700 dark:text-slate-300">
First Name
</label>
<input
id="firstName"
type="text"
name="firstName"
placeholder="First Name"
bind:value={firstName}
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500 dark:border-slate-700 dark:text-slate-100"
/>
</div>
<div class="flex flex-col gap-1.5">
<label for="lastName" class="text-sm font-medium text-slate-700 dark:text-slate-300">
Last Name
</label>
<input
id="lastName"
type="text"
name="lastName"
placeholder="Last Name"
bind:value={lastName}
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500 dark:border-slate-700 dark:text-slate-100"
/>
</div>
</div>
<div class="flex flex-col gap-1.5">
<label for="phoneNumber" class="text-sm font-medium text-slate-700 dark:text-slate-300">
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 dark:border-slate-700 dark:text-slate-100"
/>
</div>
</div>
<button
type="submit"
class="mt-1 flex h-11 w-full items-center justify-center rounded-lg bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700"
>
Submit
</button>
</form>
</Window>
@@ -27,7 +27,7 @@ import { load, actions } from './+page.server';
const invokeDefault = (request: Request) => actions.default({ request } as any);
describe('account setup page server load', () => {
describe('profile setup page server load', () => {
beforeEach(() => {
create.mockReset();
});
@@ -48,7 +48,7 @@ describe('account setup page server load', () => {
});
});
describe('account setup page server action', () => {
describe('profile setup page server action', () => {
beforeEach(() => {
create.mockReset();
});
+4 -4
View File
@@ -9,14 +9,14 @@ export const load: LayoutServerLoad = async ({ locals, url }) => {
const user = await prisma.user.findUnique({
where: { id: locals.user.id },
select: { id: true, roleId: true, profile: { select: { id: true } } }
select: { id: true, roleId: true, profiles: { select: { id: true } } }
});
if (!user) {
throw error(403, 'not authorized');
}
if (!user.profile && !url.pathname.startsWith('/account/setup')) {
// Skip redirect when already on /account/setup to avoid redirect loop
throw redirect(303, '/account/setup');
if (user.profiles.length === 0 && !url.pathname.startsWith('/profile/setup')) {
// Skip redirect when already on /profile/setup to avoid redirect loop
throw redirect(303, '/profile/setup');
}
return { userRoleId: user.roleId };
+5 -68
View File
@@ -1,74 +1,11 @@
<script lang="ts">
import { page } from '$app/state';
import { canAccessAdmin } from '$lib/svelte-admin/roles';
import { getAllModelConfigs } from '$lib/svelte-admin/registry';
import type { LayoutData } from './$types';
import type { Snippet } from 'svelte';
let { children, data } = $props<{ children: any; data: LayoutData }>();
const isAdmin = $derived(canAccessAdmin(data.userRoleId));
const adminModels = getAllModelConfigs();
const navLinks = [
{ href: '/dashboard', label: 'Dashboard' },
{ href: '/account', label: 'Account' },
{ href: '/proclamations', label: 'Proclamations' },
{ href: '/license-keys', label: 'License Keys' }
];
function isActive(href: string): boolean {
return page.url.pathname === href || page.url.pathname.startsWith(href + '/');
}
let { children }: { children: Snippet } = $props();
</script>
<div class="flex min-h-0 flex-1">
<!-- Sidebar -->
<nav class="mt-12 mr-12 flex flex-col gap-1">
{#each navLinks as link}
<a
href={link.href}
class="rounded px-3 py-1.5 text-sm font-medium transition-colors {isActive(link.href)
? 'bg-blue-50 text-blue-700 dark:bg-gray-800 dark:text-gray-100'
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100'}"
>
{link.label}
</a>
{/each}
{#if isAdmin}
<div class="mt-4 flex flex-col gap-1">
<a
href="/admin"
class="rounded px-3 py-1.5 text-sm font-medium transition-colors {isActive('/admin')
? 'bg-blue-50 text-blue-700 dark:bg-gray-800 dark:text-gray-100'
: 'text-gray-700 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100'}"
>
Admin Panel
</a>
<div class="ml-3 flex flex-col gap-0.5">
{#each adminModels as model}
<a
href="/admin/{model.slug}"
class="rounded px-3 py-1 text-sm transition-colors {isActive('/admin/' + model.slug)
? 'bg-blue-50 text-blue-700 dark:bg-gray-800 dark:text-gray-100'
: 'text-blue-600 hover:bg-gray-50 hover:text-blue-800 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100'}"
>
{model.displayName}
</a>
{/each}
<a
href="/admin/api-keys"
class="rounded px-3 py-1 text-sm transition-colors {isActive('/admin/api-keys')
? 'bg-blue-50 text-blue-700 dark:bg-gray-800 dark:text-gray-100'
: 'text-blue-600 hover:bg-gray-50 hover:text-blue-800 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100'}"
>
API Keys
</a>
</div>
</div>
{/if}
</nav>
<!-- Page content -->
<div class="min-h-0 flex-1 overflow-auto">
<div class="min-h-screen bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-slate-100">
<main class="mx-auto w-full max-w-6xl px-4 py-6 sm:px-6 sm:py-8">
{@render children()}
</div>
</main>
</div>
+5 -9
View File
@@ -1,25 +1,21 @@
<script lang="ts">
import { page } from '$app/stores';
import { resolve } from '$app/paths';
import Window from '$lib/components/containers/Window.svelte';
</script>
<Window>
<div class="flex flex-col items-center gap-4 p-6 text-center">
<h1 class="text-xl font-bold text-gray-900 dark:text-gray-100">
<div>
<h1>
Admin Error ({$page.status})
</h1>
<p class="max-w-md text-sm text-gray-600 dark:text-gray-400">
<p>
{#if $page.error?.message}
{$page.error.message}
{:else}
An unexpected error occurred while loading this admin page.
{/if}
</p>
<a
href="/admin"
class="mt-2 border border-gray-200 px-4 py-2 text-sm font-medium text-gray-900 hover:bg-gray-50 dark:border-gray-700 dark:text-gray-100 dark:hover:bg-gray-800"
>
Back to Admin
</a>
<a href={resolve('/admin')}> Back to Admin </a>
</div>
</Window>
@@ -1,7 +1,6 @@
import type { LayoutServerLoad } from './$types';
import { error } from '@sveltejs/kit';
import { prisma } from '$lib/db';
import { canAccessAdmin } from '$lib/svelte-admin/roles';
/**
* This Layout Server-Side page checks if the user is logged in
@@ -17,7 +16,7 @@ export const load: LayoutServerLoad = async ({ locals }) => {
select: { id: true, name: true, roleId: true, role: { select: { name: true } } }
});
if (!user || !canAccessAdmin(user.roleId)) {
if (!user || !user.roleId || user.roleId > 3) {
console.log(
`Attempted to access /admin via ${user?.id} : ${user?.name} with a role of ${user?.role}`
);
+3 -6
View File
@@ -1,8 +1,5 @@
import type { PageServerLoad } from './$types';
import { getSerializableModelConfigs } from '$lib/svelte-admin/registry';
import { redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ locals }) => {
const viewerRoleId = (locals.user as { roleId?: number } | null)?.roleId ?? 4;
const models = getSerializableModelConfigs(viewerRoleId, ['license-keys']);
return { models };
export const load = () => {
redirect(307, '/admin/profiles');
};
-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>
@@ -1,72 +0,0 @@
import type { PageServerLoad } from './$types';
import { error } from '@sveltejs/kit';
import { prisma } from '$lib/db';
import { rotateApiKey } from '$lib/server/api-key';
import { ROLES } from '$lib/svelte-admin/roles';
export const load: PageServerLoad = async ({ locals, parent }) => {
const { adminUser } = await parent();
if (adminUser.roleId > ROLES.SUPER) {
throw error(403, 'Only Super admins can manage API keys');
}
const now = new Date();
const activeKey = await prisma.apiKey.findFirst({
where: {
revokedAt: null,
OR: [{ expiresAt: null }, { expiresAt: { gt: now } }]
},
select: { keyPrefix: true, createdAt: true, expiresAt: true },
orderBy: { createdAt: 'desc' }
});
const expiredCount = await prisma.apiKey.count({
where: {
OR: [{ revokedAt: { not: null, lte: now } }, { expiresAt: { not: null, lte: now } }]
}
});
return {
activeKey: activeKey
? {
prefix: activeKey.keyPrefix,
createdAt: activeKey.createdAt.toISOString(),
expiresAt: activeKey.expiresAt?.toISOString() ?? null
}
: null,
totalRotated: expiredCount
};
};
async function viewerRoleId(locals: App.Locals): Promise<number> {
if (!locals.user?.id) return 4;
const user = await prisma.user.findUnique({
where: { id: locals.user.id },
select: { roleId: true }
});
return user?.roleId ?? 4;
}
export const actions = {
rotate: async ({ locals }) => {
if ((await viewerRoleId(locals)) > ROLES.SUPER) {
return { success: false, error: 'Not authorized' };
}
const result = await rotateApiKey(false);
return {
success: true,
newKey: result.plaintext,
mode: 'normal' as const,
expiresAt: result.previousExpiresAt?.toISOString() ?? null
};
},
rotateEmergency: async ({ locals }) => {
if ((await viewerRoleId(locals)) > ROLES.SUPER) {
return { success: false, error: 'Not authorized' };
}
const result = await rotateApiKey(true);
return { success: true, newKey: result.plaintext, mode: 'emergency' as const };
}
};

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