Compare commits

..
7 Commits
Author SHA1 Message Date
NeilClack 9e9cabfb2b Update docs and formatting; remove obsolete v2 schema doc
Build and Push to Digital Ocean Container Registry / build-and-push (push) Successful in 1m42s
Build and Push to Digital Ocean Container Registry / migrate (push) Failing after 22s
- Refresh AI guide docs (api, svelte-patterns, testing-patterns)
- Add API updates notes, drop the outdated portal_api_v2_schema.md
- Format package.json
2026-08-27 12:07:34 -05:00
NeilClack 4373458272 Allow profiles and license keys without a user; link them on registration
Admins can now create a Profile or License Key for someone who hasn't
registered yet (matched by email). When that person registers, any
unattached profile and license keys are linked to their new account.

Also remove the Proclamations surface from the user dashboard.
2026-08-27 12:04:09 -05:00
NeilClack b76a540dc4 Add mobile-first styling to layouts, auth, and user pages
- Consolidated navbar with hamburger menu for mobile
- Responsive styling for root/protected layouts and auth pages
- Mobile-friendly account, dashboard, license-key, and setup pages
2026-08-27 12:03:59 -05:00
NeilClack 1d11de8cec Refactor admin to shared generic CRUD; restructure proclamation categories/groups and APIs
- Replace per-model admin forms with the shared generic AdminTable/AdminModal CRUD
- Add ProclamationGroup model and admin + API v2 routes (replaces subscription levels)
- Drop subscription levels and flatten proclamation categories
- Update v2 serializers and seed data accordingly
2026-08-27 12:03:40 -05:00
NeilClack b618d8cb93 Update data model: flat proclamation groups, drop subscription levels; allow profile/license key without user 2026-08-27 12:03:27 -05:00
NeilClack f4e736c92d Update builder name
Build and Push to Digital Ocean Container Registry / build-and-push (push) Successful in 1m56s
Build and Push to Digital Ocean Container Registry / migrate (push) Failing after 10s
2026-08-18 20:14:44 -05:00
NeilClack 5096118ef5 Remove user proclamations page
Users should not be able to view proclamations from the web portal.
2026-08-18 13:38:05 -05:00
105 changed files with 4503 additions and 7879 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
migrate:
needs: build-and-push
runs-on: ubuntu-latest
runs-on: build-server
steps:
- name: Checkout
+27 -30
View File
@@ -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.
@@ -113,15 +113,15 @@ portal.Rhemawavetech.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 -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().
+330
View File
@@ -0,0 +1,330 @@
# API Updates — Category Flattening, Groups, Subscription Level Removal
**Release:** v2 schema change (migration `20260821000000_flat_categories_groups_drop_subscription_levels`)
**Status:** Breaking changes. Clients must be updated.
## Summary of changes
| Endpoint | Change |
| ----------------------------------------- | ------------------------------------------------------------------------------------ |
| `GET /api/v2/categories` | **Breaking**: `parentId` removed from every record. Categories are now a flat list. |
| `GET /api/v2/groups` | **New**: returns the group list (former sub-categories). |
| `GET /api/v2/proclamations` | **Breaking**: `groupId` added; `subscriptionLevelId` removed. |
| `GET /api/v2/subscription-levels` | **Removed**: subscription levels no longer exist. |
| `POST /api/v2/license` | **Breaking**: response no longer contains `subscription_level`. |
| `POST /api/v2/automations/create/account` | **Breaking**: `subscription_level` is no longer accepted or required in the payload. |
| `POST /api/v1/license` | **Breaking**: response no longer contains `subscription_level`. |
| `POST /api/v1/automations/create/account` | **Breaking**: `subscription_level` is no longer accepted or required in the payload. |
| `GET /api/v1/categories` | Payload implicitly changed: rows no longer contain `parentId`. |
### Data model mapping
- Former **parent** categories (`Spiritual`, `Physical`, `Mental`, `Emotional`, `Financial`,
`Social`, `Biblical`, ...) became the flat **Category** list.
- Former **child** sub-categories (`General`, `Specific`, `Precise`, `Prayers`, `Scripture`)
became **Groups**.
- A proclamation's former child category becomes its `group`; its former parent category becomes
its `category`. A proclamation may move between categories without changing its group.
- Proclamations that had no sub-category (e.g. under `Custom (Client Specific)`) were assigned to
the `General` group.
- Every proclamation now has exactly one category and one group.
---
## OpenAPI 3.0 specification
```yaml
openapi: 3.0.3
info:
title: Rhemawave Portal API
version: 2.1.0
description: |
Changes in this release:
* ProclamationCategory hierarchy flattened; parentId removed everywhere.
* New ProclamationGroup resource replaces sub-categories as a movable filter.
* SubscriptionLevel resource and all level-based gating removed.
paths:
/api/v2/categories:
get:
summary: List categories (flat)
description: >
Returns all proclamation categories as a flat, ordered list.
BREAKING: the previous version returned a hierarchical list where each
record carried a nullable parentId linking children to parents. That
field no longer exists.
tags: [Categories]
responses:
'200':
description: Flat category list.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Category'
examples:
newResponse:
summary: New response
value:
- id: 4
name: Spiritual
sortOrder: 0
- id: 2
name: Physical
sortOrder: 1
oldResponseDeprecated:
summary: Old (removed) response shape
value:
- id: 5
name: General
sortOrder: 0
parentId: 4
- id: 9
name: Specific
sortOrder: 1
parentId: 4
/api/v2/groups:
get:
summary: List groups
description: >
NEW ENDPOINT. Returns all proclamation groups ordered by sortOrder.
Groups replace the former sub-categories and act as a movable filter:
a proclamation can be moved from one category to another while keeping
its group.
tags: [Groups]
responses:
'200':
description: Group list.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Group'
example:
- id: 1
name: General
sortOrder: 0
- id: 2
name: Specific
sortOrder: 1
/api/v2/proclamations:
get:
summary: List proclamations
description: >
Returns proclamations with their canonical frequency id and optional
color palette. BREAKING: each record now carries groupId (required,
always present) and no longer carries subscriptionLevelId. When the
optional entitlement headers are supplied, only the caller's entitled
proclamations are returned; there is no longer any per-level gating.
tags: [Proclamations]
parameters:
- in: header
name: x-license-key
required: false
schema: { type: string }
description: License key. Must be paired with x-email.
- in: header
name: x-email
required: false
schema: { type: string }
description: Account email. Must be paired with x-license-key.
responses:
'200':
description: Proclamation list.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Proclamation'
examples:
newResponse:
summary: New response
value:
- id: b40fdcb9-703c-420e-af32-a081964c6f08
name: Peace & Relaxation
content: <p>The text content</p>
categoryId: 4
groupId: 1
solfeggioFrequencyId: 5
colorPalette:
[
'#32CD32',
'#3CB371',
'#90EE90',
'#98FB98',
'#ADFFA9',
'#C8FFD2',
'#F0FFF0'
]
oldResponseDeprecated:
summary: Old (removed) response shape
value:
- id: b40fdcb9-703c-420e-af32-a081964c6f08
name: Peace & Relaxation
content: <p>The text content</p>
categoryId: 5
solfeggioFrequencyId: 5
subscriptionLevelId: 1
'400':
description: Entitlement headers incomplete or invalid.
/api/v2/subscription-levels:
get:
summary: REMOVED
deprecated: true
description: >
This endpoint was removed. Subscription levels no longer exist in the
data model; clients must stop calling it.
tags: [Removed]
responses:
'404':
description: Route no longer exists.
/api/v2/license:
post:
summary: Validate a license key
description: >
Validates a key/email pair. BREAKING: the response no longer contains a
subscription_level object.
tags: [License]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [license_key, email_address]
properties:
license_key: { type: string, example: ABCD-EFGH-IJKL-MNOP }
email_address: { type: string, format: email }
responses:
'200':
description: Key is valid.
content:
application/json:
schema:
$ref: '#/components/schemas/LicenseResponse'
examples:
newResponse:
summary: New response
value:
key: ABCD-EFGH-IJKL-MNOP
email_address: user@example.com
oldResponseDeprecated:
summary: Old (removed) response shape
value:
key: ABCD-EFGH-IJKL-MNOP
email_address: user@example.com
subscription_level:
id: 2
name: Advanced
description: General + Specific
'400':
description: Validation failure (missing fields, unknown key, wrong owner).
/api/v2/automations/create/account:
post:
summary: Create account + license key (GoHighLevel automation)
description: >
Creates a portal account when needed and issues a license key.
BREAKING: subscription_level was removed from the request contract. The
field is neither required nor validated anymore; sending it is ignored.
Signature headers (x-timestamp, x-nonce, x-signature) are unchanged.
tags: [Automations]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email]
properties:
email: { type: string, format: email }
name: { type: string }
responses:
'200':
description: Account and license key created.
'400':
description: Missing/invalid email.
'401':
description: Invalid signature.
components:
schemas:
Category:
type: object
required: [id, name, sortOrder]
properties:
id: { type: integer }
name: { type: string }
sortOrder: { type: integer }
Group:
type: object
required: [id, name, sortOrder]
properties:
id: { type: integer }
name: { type: string }
sortOrder: { type: integer }
Proclamation:
type: object
required: [id, name, content, categoryId, groupId, solfeggioFrequencyId]
properties:
id:
type: string
format: uuid
name: { type: string }
content: { type: string, description: HTML formatted content. }
categoryId:
type: integer
description: Flat category this proclamation belongs to.
groupId:
type: integer
description: >
Group membership (movable filter). Independent of categoryId;
changing the category does not change the group.
solfeggioFrequencyId:
type: integer
minimum: 1
maximum: 9
description: Canonical kiosk frequency id (not the DB autoincrement id).
colorPalette:
type: array
items: { type: string }
minItems: 7
maxItems: 7
description: Present only when a palette exists for the frequency.
LicenseResponse:
type: object
required: [key, email_address]
properties:
key: { type: string }
email_address: { type: string, format: email }
```
---
## Client migration checklist
1. **Kiosk / API consumers**
- Stop reading `parentId`; treat `/api/v2/categories` as flat.
- Fetch `/api/v2/groups` once at startup for the filter list.
- Read `groupId` from each proclamation record for filtering.
- Remove all references to `subscriptionLevelId` and `/api/v2/subscription-levels`.
- Remove `subscription_level` handling from license validation responses.
2. **GoHighLevel automation**
- Remove `subscription_level` from the account-creation webhook payload
(optional but recommended; the field is now ignored).
3. **Admin portal**
- Manage categories under Admin → Proclamation Categories (flat list, drag to reorder).
- Manage groups under Admin → Proclamation Groups (drag to reorder).
- Proclamations now select one Category and one Group independently.
+1 -1
View File
@@ -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.
@@ -0,0 +1,81 @@
-- Flatten ProclamationCategory hierarchy into a flat list, introduce
-- ProclamationGroup (formerly child sub-categories), and remove
-- SubscriptionLevel entirely.
-- 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;
-- Remove former child categories (subscription-level join rows cascade)
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");
-- Remove subscription levels entirely
DROP TABLE "ProclamationCategorySubscriptionLevel";
-- DropForeignKey
ALTER TABLE "Proclamation" DROP CONSTRAINT "Proclamation_subscriptionLevelId_fkey";
-- AlterTable
ALTER TABLE "Proclamation" DROP COLUMN "subscriptionLevelId";
-- DropTable
DROP TABLE "SubscriptionLevel";
-- AlterTable
ALTER TABLE "LicenseKey" DROP COLUMN "level";
@@ -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;
+40 -50
View File
@@ -25,7 +25,7 @@ model 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,16 @@ 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])
}
model ProclamationGroup {
id Int @id @default(autoincrement())
name String @unique
sortOrder Int @default(0)
proclamations Proclamation[]
}
model UserProclamations {
@@ -142,40 +153,27 @@ 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])
}
// Join table for ProclamationCategory -> SubscriptionLevel defaults
model ProclamationCategorySubscriptionLevel {
categoryId Int
category ProclamationCategory @relation(fields: [categoryId], references: [id], onDelete: Cascade)
subscriptionLevelId Int
subscriptionLevel SubscriptionLevel @relation(fields: [subscriptionLevelId], references: [id], onDelete: Cascade)
@@id([categoryId, subscriptionLevelId])
id Int @id @default(autoincrement())
name String @unique
sortOrder Int @default(0)
proclamations Proclamation[]
}
model LicenseKey {
id String @id @default(uuid())
key String @unique
active Boolean
level String
userId String
user User @relation(fields: [userId], references: [id])
userId String?
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
email String?
@@index([userId])
@@index([email])
}
model ApiKey {
@@ -187,14 +185,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 +193,4 @@ model ColorPalette {
colors Json // ["#RRGGBB", ... x7]
@@map("ColorPalette")
}
}
+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({
+2 -15
View File
@@ -3,23 +3,10 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function () {
const theme = localStorage.getItem('theme');
if (
theme === 'dark' ||
(!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
})();
</script>
%sveltekit.head%
</head>
<body class="m-0" data-sveltekit-preload-data="hover">
<div class="flex h-dvh w-dvw flex-col">%sveltekit.body%</div>
<body data-sveltekit-preload-data="hover">
<div>%sveltekit.body%</div>
</body>
</html>
+8 -28
View File
@@ -43,38 +43,18 @@
</script>
{#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}
onkeydown={handleKeydown}
>
<div
class="my-8 w-full max-w-2xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900"
>
<div
class="flex items-center justify-between border-b border-gray-200 px-6 py-4 dark:border-gray-700"
>
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">{title}</h2>
<div class="flex items-center gap-2">
<div role="presentation" onclick={handleBackdrop} onkeydown={handleKeydown}>
<div>
<div>
<h2>{title}</h2>
<div>
{#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"
>
Delete
</button>
<button type="button" onclick={confirmDelete}> Delete </button>
{/if}
<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
>
<button type="button" onclick={onClose}>&times;</button>
</div>
</div>
<div class="px-6 py-4">
<div>
{@render children()}
</div>
</div>
+15 -48
View File
@@ -54,8 +54,6 @@
: filtered
);
let isDragging = $state(false);
let dragOverride = $state<Record<string, unknown>[] | null>(null);
let rows = $derived(dragOverride ?? sorted);
@@ -74,7 +72,6 @@
function handleDragStart(e: DragEvent, i: number) {
dragOverride = sorted;
isDragging = true;
dragIndex = i;
hoverIndex = i;
e.dataTransfer!.effectAllowed = 'move';
@@ -99,14 +96,12 @@
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;
}
@@ -131,45 +126,32 @@
}
</script>
<div class="flex flex-col gap-4">
<div class="flex items-center justify-between gap-4">
<div>
<div>
{#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"
/>
<input type="text" placeholder="Search {singular}s..." bind:value={search} />
{: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"
>
<button type="button" onclick={onNew}>
New {singular} +
</button>
{/if}
</div>
<div class="overflow-x-auto border border-gray-200 dark:border-gray-700">
<table class="w-full">
<div>
<table>
<thead>
<tr class="border-b border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-800">
<tr>
{#if draggable}
<th scope="col" class="w-10 px-2 py-2"></th>
<th scope="col"></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"
>
<th scope="col" onclick={() => toggleSort(field)}>
{fieldNameToLabel(field)}
{#if sortCol === field}
<span class="ml-1">{sortAsc ? '\u25B2' : '\u25BC'}</span>
<span>{sortAsc ? '\u25B2' : '\u25BC'}</span>
{/if}
</th>
{/each}
@@ -184,25 +166,15 @@
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 onclick={(e) => e.stopPropagation()}>
<span>⠿</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">
<td>
<span>
{formatCellValue(resolveFieldValue(record, field), field)}
{#if copyableFields.includes(field)}
<button
@@ -212,10 +184,8 @@
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"
@@ -234,10 +204,7 @@
</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"
>
<td colspan={listFields.length + (draggable ? 1 : 0)}>
No {singular}s found.
</td>
</tr>
+5 -19
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);
@@ -30,17 +24,9 @@
</script>
{#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'}"
>
<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
>
<div role="alert">
<span>{message}</span>
<span>{remaining}s</span>
<button type="button" onclick={dismiss}>&times;</button>
</div>
{/if}
+4 -2
View File
@@ -2,6 +2,8 @@
let { children } = $props();
</script>
<div class="flex min-h-0 flex-1 items-center justify-center overflow-auto">
{@render children()}
<div class="mx-auto w-full max-w-md px-4 py-8 sm:py-12">
<div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8">
{@render children()}
</div>
</div>
-103
View File
@@ -1,103 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
import Highlight from '@tiptap/extension-highlight';
import { TextStyle } from '@tiptap/extension-text-style';
import { Color } from '@tiptap/extension-color';
let { initialContent = '', name = 'content' }: { initialContent?: string; name?: string } =
$props();
let element: HTMLElement;
let editor: Editor;
let content = $state('');
onMount(() => {
content = initialContent;
editor = new Editor({
element,
extensions: [
StarterKit.configure({
bulletList: false,
orderedList: false
}),
TextStyle,
Color,
Highlight.configure({ multicolor: true })
],
content: initialContent,
editorProps: {
attributes: {
class:
'prose prose-sm max-w-none outline-none h-full min-h-40 p-3 dark:prose-invert dark:text-gray-100'
}
},
onUpdate: ({ editor }) => {
content = editor.getHTML();
}
});
editor.setEditable(true);
return () => editor.destroy();
});
function toggleBold() {
editor?.chain().focus().toggleBold().run();
}
function toggleItalic() {
editor?.chain().focus().toggleItalic().run();
}
function toggleHighlight() {
editor?.chain().focus().toggleHighlight().run();
}
function setColor() {
const current = editor?.getAttributes('textStyle').color ?? '#000000';
const colors = ['#000000', '#FF0000', '#0000FF', '#008000', '#FFA500', '#800080', '#808080'];
const idx = colors.indexOf(current);
const next = colors[(idx + 1) % colors.length];
editor?.chain().focus().setColor(next).run();
}
</script>
<div class="border border-gray-300 dark:border-gray-600">
<div
class="flex gap-1 border-b border-gray-300 bg-gray-100 p-2 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300"
>
<button
type="button"
onclick={toggleBold}
class="min-w-8 px-2 py-1 font-bold hover:bg-gray-200 dark:hover:bg-gray-700"
title="Bold">B</button
>
<button
type="button"
onclick={toggleItalic}
class="min-w-8 px-2 py-1 italic hover:bg-gray-200 dark:hover:bg-gray-700"
title="Italic">I</button
>
<button
type="button"
onclick={toggleHighlight}
class="min-w-8 px-2 py-1 hover:bg-gray-200 dark:hover:bg-gray-700"
title="Highlight"><span class="bg-yellow-200 px-1 dark:bg-yellow-600">H</span></button
>
<button
type="button"
onclick={setColor}
class="min-w-8 px-2 py-1 hover:bg-gray-200 dark:hover:bg-gray-700"
title="Cycle text color">A</button
>
</div>
<div
bind:this={element}
role="textbox"
tabindex="0"
contenteditable="true"
class="min-h-40 cursor-text bg-white dark:bg-gray-900"
></div>
</div>
<input type="hidden" {name} value={content} />
+5 -10
View File
@@ -57,6 +57,11 @@ export type Verification = Prisma.VerificationModel
*
*/
export type Proclamation = Prisma.ProclamationModel
/**
* Model ProclamationGroup
*
*/
export type ProclamationGroup = Prisma.ProclamationGroupModel
/**
* Model UserProclamations
*
@@ -72,11 +77,6 @@ export type SolfeggioFrequency = Prisma.SolfeggioFrequencyModel
*
*/
export type ProclamationCategory = Prisma.ProclamationCategoryModel
/**
* Model ProclamationCategorySubscriptionLevel
*
*/
export type ProclamationCategorySubscriptionLevel = Prisma.ProclamationCategorySubscriptionLevelModel
/**
* Model LicenseKey
*
@@ -87,11 +87,6 @@ export type LicenseKey = Prisma.LicenseKeyModel
*
*/
export type ApiKey = Prisma.ApiKeyModel
/**
* Model SubscriptionLevel
*
*/
export type SubscriptionLevel = Prisma.SubscriptionLevelModel
/**
* Model ColorPalette
*
+5 -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,11 +111,6 @@ export type LicenseKey = Prisma.LicenseKeyModel
*
*/
export type ApiKey = Prisma.ApiKeyModel
/**
* Model SubscriptionLevel
*
*/
export type SubscriptionLevel = Prisma.SubscriptionLevelModel
/**
* Model ColorPalette
*
File diff suppressed because one or more lines are too long
@@ -392,13 +392,12 @@ export const ModelName = {
UserRole: 'UserRole',
Verification: 'Verification',
Proclamation: 'Proclamation',
ProclamationGroup: 'ProclamationGroup',
UserProclamations: 'UserProclamations',
SolfeggioFrequency: 'SolfeggioFrequency',
ProclamationCategory: 'ProclamationCategory',
ProclamationCategorySubscriptionLevel: 'ProclamationCategorySubscriptionLevel',
LicenseKey: 'LicenseKey',
ApiKey: 'ApiKey',
SubscriptionLevel: 'SubscriptionLevel',
ColorPalette: 'ColorPalette'
} as const
@@ -415,7 +414,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
omit: GlobalOmitOptions
}
meta: {
modelProps: "user" | "profile" | "address" | "session" | "account" | "userRole" | "verification" | "proclamation" | "userProclamations" | "solfeggioFrequency" | "proclamationCategory" | "proclamationCategorySubscriptionLevel" | "licenseKey" | "apiKey" | "subscriptionLevel" | "colorPalette"
modelProps: "user" | "profile" | "address" | "session" | "account" | "userRole" | "verification" | "proclamation" | "proclamationGroup" | "userProclamations" | "solfeggioFrequency" | "proclamationCategory" | "licenseKey" | "apiKey" | "colorPalette"
txIsolationLevel: TransactionIsolationLevel
}
model: {
@@ -1011,6 +1010,80 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
}
}
}
ProclamationGroup: {
payload: Prisma.$ProclamationGroupPayload<ExtArgs>
fields: Prisma.ProclamationGroupFieldRefs
operations: {
findUnique: {
args: Prisma.ProclamationGroupFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload> | null
}
findUniqueOrThrow: {
args: Prisma.ProclamationGroupFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>
}
findFirst: {
args: Prisma.ProclamationGroupFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload> | null
}
findFirstOrThrow: {
args: Prisma.ProclamationGroupFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>
}
findMany: {
args: Prisma.ProclamationGroupFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>[]
}
create: {
args: Prisma.ProclamationGroupCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>
}
createMany: {
args: Prisma.ProclamationGroupCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.ProclamationGroupCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>[]
}
delete: {
args: Prisma.ProclamationGroupDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>
}
update: {
args: Prisma.ProclamationGroupUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>
}
deleteMany: {
args: Prisma.ProclamationGroupDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.ProclamationGroupUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.ProclamationGroupUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>[]
}
upsert: {
args: Prisma.ProclamationGroupUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationGroupPayload>
}
aggregate: {
args: Prisma.ProclamationGroupAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateProclamationGroup>
}
groupBy: {
args: Prisma.ProclamationGroupGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.ProclamationGroupGroupByOutputType>[]
}
count: {
args: Prisma.ProclamationGroupCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.ProclamationGroupCountAggregateOutputType> | number
}
}
}
UserProclamations: {
payload: Prisma.$UserProclamationsPayload<ExtArgs>
fields: Prisma.UserProclamationsFieldRefs
@@ -1233,80 +1306,6 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
}
}
}
ProclamationCategorySubscriptionLevel: {
payload: Prisma.$ProclamationCategorySubscriptionLevelPayload<ExtArgs>
fields: Prisma.ProclamationCategorySubscriptionLevelFieldRefs
operations: {
findUnique: {
args: Prisma.ProclamationCategorySubscriptionLevelFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload> | null
}
findUniqueOrThrow: {
args: Prisma.ProclamationCategorySubscriptionLevelFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>
}
findFirst: {
args: Prisma.ProclamationCategorySubscriptionLevelFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload> | null
}
findFirstOrThrow: {
args: Prisma.ProclamationCategorySubscriptionLevelFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>
}
findMany: {
args: Prisma.ProclamationCategorySubscriptionLevelFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>[]
}
create: {
args: Prisma.ProclamationCategorySubscriptionLevelCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>
}
createMany: {
args: Prisma.ProclamationCategorySubscriptionLevelCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.ProclamationCategorySubscriptionLevelCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>[]
}
delete: {
args: Prisma.ProclamationCategorySubscriptionLevelDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>
}
update: {
args: Prisma.ProclamationCategorySubscriptionLevelUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>
}
deleteMany: {
args: Prisma.ProclamationCategorySubscriptionLevelDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.ProclamationCategorySubscriptionLevelUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.ProclamationCategorySubscriptionLevelUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>[]
}
upsert: {
args: Prisma.ProclamationCategorySubscriptionLevelUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ProclamationCategorySubscriptionLevelPayload>
}
aggregate: {
args: Prisma.ProclamationCategorySubscriptionLevelAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateProclamationCategorySubscriptionLevel>
}
groupBy: {
args: Prisma.ProclamationCategorySubscriptionLevelGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.ProclamationCategorySubscriptionLevelGroupByOutputType>[]
}
count: {
args: Prisma.ProclamationCategorySubscriptionLevelCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.ProclamationCategorySubscriptionLevelCountAggregateOutputType> | number
}
}
}
LicenseKey: {
payload: Prisma.$LicenseKeyPayload<ExtArgs>
fields: Prisma.LicenseKeyFieldRefs
@@ -1455,80 +1454,6 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
}
}
}
SubscriptionLevel: {
payload: Prisma.$SubscriptionLevelPayload<ExtArgs>
fields: Prisma.SubscriptionLevelFieldRefs
operations: {
findUnique: {
args: Prisma.SubscriptionLevelFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload> | null
}
findUniqueOrThrow: {
args: Prisma.SubscriptionLevelFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>
}
findFirst: {
args: Prisma.SubscriptionLevelFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload> | null
}
findFirstOrThrow: {
args: Prisma.SubscriptionLevelFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>
}
findMany: {
args: Prisma.SubscriptionLevelFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>[]
}
create: {
args: Prisma.SubscriptionLevelCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>
}
createMany: {
args: Prisma.SubscriptionLevelCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.SubscriptionLevelCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>[]
}
delete: {
args: Prisma.SubscriptionLevelDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>
}
update: {
args: Prisma.SubscriptionLevelUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>
}
deleteMany: {
args: Prisma.SubscriptionLevelDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.SubscriptionLevelUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.SubscriptionLevelUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>[]
}
upsert: {
args: Prisma.SubscriptionLevelUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>
}
aggregate: {
args: Prisma.SubscriptionLevelAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateSubscriptionLevel>
}
groupBy: {
args: Prisma.SubscriptionLevelGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.SubscriptionLevelGroupByOutputType>[]
}
count: {
args: Prisma.SubscriptionLevelCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.SubscriptionLevelCountAggregateOutputType> | number
}
}
}
ColorPalette: {
payload: Prisma.$ColorPalettePayload<ExtArgs>
fields: Prisma.ColorPaletteFieldRefs
@@ -1659,6 +1584,7 @@ export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof User
export const ProfileScalarFieldEnum = {
id: 'id',
userId: 'userId',
email: 'email',
firstName: 'firstName',
lastName: 'lastName',
phoneNumber: 'phoneNumber',
@@ -1744,12 +1670,21 @@ export const ProclamationScalarFieldEnum = {
content: 'content',
solfeggioFrequencyId: 'solfeggioFrequencyId',
categoryId: 'categoryId',
subscriptionLevelId: 'subscriptionLevelId'
groupId: 'groupId'
} as const
export type ProclamationScalarFieldEnum = (typeof ProclamationScalarFieldEnum)[keyof typeof ProclamationScalarFieldEnum]
export const ProclamationGroupScalarFieldEnum = {
id: 'id',
name: 'name',
sortOrder: 'sortOrder'
} as const
export type ProclamationGroupScalarFieldEnum = (typeof ProclamationGroupScalarFieldEnum)[keyof typeof ProclamationGroupScalarFieldEnum]
export const UserProclamationsScalarFieldEnum = {
userId: 'userId',
proclamationId: 'proclamationId',
@@ -1772,27 +1707,18 @@ export type SolfeggioFrequencyScalarFieldEnum = (typeof SolfeggioFrequencyScalar
export const ProclamationCategoryScalarFieldEnum = {
id: 'id',
name: 'name',
sortOrder: 'sortOrder',
parentId: 'parentId'
sortOrder: 'sortOrder'
} as const
export type ProclamationCategoryScalarFieldEnum = (typeof ProclamationCategoryScalarFieldEnum)[keyof typeof ProclamationCategoryScalarFieldEnum]
export const ProclamationCategorySubscriptionLevelScalarFieldEnum = {
categoryId: 'categoryId',
subscriptionLevelId: 'subscriptionLevelId'
} as const
export type ProclamationCategorySubscriptionLevelScalarFieldEnum = (typeof ProclamationCategorySubscriptionLevelScalarFieldEnum)[keyof typeof ProclamationCategorySubscriptionLevelScalarFieldEnum]
export const LicenseKeyScalarFieldEnum = {
id: 'id',
key: 'key',
active: 'active',
level: 'level',
userId: 'userId'
userId: 'userId',
email: 'email'
} as const
export type LicenseKeyScalarFieldEnum = (typeof LicenseKeyScalarFieldEnum)[keyof typeof LicenseKeyScalarFieldEnum]
@@ -1810,15 +1736,6 @@ export const ApiKeyScalarFieldEnum = {
export type ApiKeyScalarFieldEnum = (typeof ApiKeyScalarFieldEnum)[keyof typeof ApiKeyScalarFieldEnum]
export const SubscriptionLevelScalarFieldEnum = {
id: 'id',
name: 'name',
description: 'description'
} as const
export type SubscriptionLevelScalarFieldEnum = (typeof SubscriptionLevelScalarFieldEnum)[keyof typeof SubscriptionLevelScalarFieldEnum]
export const ColorPaletteScalarFieldEnum = {
solfeggioFrequencyId: 'solfeggioFrequencyId',
colors: 'colors'
@@ -2067,13 +1984,12 @@ export type GlobalOmitConfig = {
userRole?: Prisma.UserRoleOmit
verification?: Prisma.VerificationOmit
proclamation?: Prisma.ProclamationOmit
proclamationGroup?: Prisma.ProclamationGroupOmit
userProclamations?: Prisma.UserProclamationsOmit
solfeggioFrequency?: Prisma.SolfeggioFrequencyOmit
proclamationCategory?: Prisma.ProclamationCategoryOmit
proclamationCategorySubscriptionLevel?: Prisma.ProclamationCategorySubscriptionLevelOmit
licenseKey?: Prisma.LicenseKeyOmit
apiKey?: Prisma.ApiKeyOmit
subscriptionLevel?: Prisma.SubscriptionLevelOmit
colorPalette?: Prisma.ColorPaletteOmit
}
@@ -59,13 +59,12 @@ export const ModelName = {
UserRole: 'UserRole',
Verification: 'Verification',
Proclamation: 'Proclamation',
ProclamationGroup: 'ProclamationGroup',
UserProclamations: 'UserProclamations',
SolfeggioFrequency: 'SolfeggioFrequency',
ProclamationCategory: 'ProclamationCategory',
ProclamationCategorySubscriptionLevel: 'ProclamationCategorySubscriptionLevel',
LicenseKey: 'LicenseKey',
ApiKey: 'ApiKey',
SubscriptionLevel: 'SubscriptionLevel',
ColorPalette: 'ColorPalette'
} as const
@@ -102,6 +101,7 @@ export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof User
export const ProfileScalarFieldEnum = {
id: 'id',
userId: 'userId',
email: 'email',
firstName: 'firstName',
lastName: 'lastName',
phoneNumber: 'phoneNumber',
@@ -187,12 +187,21 @@ export const ProclamationScalarFieldEnum = {
content: 'content',
solfeggioFrequencyId: 'solfeggioFrequencyId',
categoryId: 'categoryId',
subscriptionLevelId: 'subscriptionLevelId'
groupId: 'groupId'
} as const
export type ProclamationScalarFieldEnum = (typeof ProclamationScalarFieldEnum)[keyof typeof ProclamationScalarFieldEnum]
export const ProclamationGroupScalarFieldEnum = {
id: 'id',
name: 'name',
sortOrder: 'sortOrder'
} as const
export type ProclamationGroupScalarFieldEnum = (typeof ProclamationGroupScalarFieldEnum)[keyof typeof ProclamationGroupScalarFieldEnum]
export const UserProclamationsScalarFieldEnum = {
userId: 'userId',
proclamationId: 'proclamationId',
@@ -215,27 +224,18 @@ export type SolfeggioFrequencyScalarFieldEnum = (typeof SolfeggioFrequencyScalar
export const ProclamationCategoryScalarFieldEnum = {
id: 'id',
name: 'name',
sortOrder: 'sortOrder',
parentId: 'parentId'
sortOrder: 'sortOrder'
} as const
export type ProclamationCategoryScalarFieldEnum = (typeof ProclamationCategoryScalarFieldEnum)[keyof typeof ProclamationCategoryScalarFieldEnum]
export const ProclamationCategorySubscriptionLevelScalarFieldEnum = {
categoryId: 'categoryId',
subscriptionLevelId: 'subscriptionLevelId'
} as const
export type ProclamationCategorySubscriptionLevelScalarFieldEnum = (typeof ProclamationCategorySubscriptionLevelScalarFieldEnum)[keyof typeof ProclamationCategorySubscriptionLevelScalarFieldEnum]
export const LicenseKeyScalarFieldEnum = {
id: 'id',
key: 'key',
active: 'active',
level: 'level',
userId: 'userId'
userId: 'userId',
email: 'email'
} as const
export type LicenseKeyScalarFieldEnum = (typeof LicenseKeyScalarFieldEnum)[keyof typeof LicenseKeyScalarFieldEnum]
@@ -253,15 +253,6 @@ export const ApiKeyScalarFieldEnum = {
export type ApiKeyScalarFieldEnum = (typeof ApiKeyScalarFieldEnum)[keyof typeof ApiKeyScalarFieldEnum]
export const SubscriptionLevelScalarFieldEnum = {
id: 'id',
name: 'name',
description: 'description'
} as const
export type SubscriptionLevelScalarFieldEnum = (typeof SubscriptionLevelScalarFieldEnum)[keyof typeof SubscriptionLevelScalarFieldEnum]
export const ColorPaletteScalarFieldEnum = {
solfeggioFrequencyId: 'solfeggioFrequencyId',
colors: 'colors'
+1 -2
View File
@@ -16,12 +16,11 @@ export type * from './models/Account.ts'
export type * from './models/UserRole.ts'
export type * from './models/Verification.ts'
export type * from './models/Proclamation.ts'
export type * from './models/ProclamationGroup.ts'
export type * from './models/UserProclamations.ts'
export type * from './models/SolfeggioFrequency.ts'
export type * from './models/ProclamationCategory.ts'
export type * from './models/ProclamationCategorySubscriptionLevel.ts'
export type * from './models/LicenseKey.ts'
export type * from './models/ApiKey.ts'
export type * from './models/SubscriptionLevel.ts'
export type * from './models/ColorPalette.ts'
export type * from './commonInputTypes.ts'
+79 -60
View File
@@ -28,24 +28,24 @@ export type LicenseKeyMinAggregateOutputType = {
id: string | null
key: string | null
active: boolean | null
level: string | null
userId: string | null
email: string | null
}
export type LicenseKeyMaxAggregateOutputType = {
id: string | null
key: string | null
active: boolean | null
level: string | null
userId: string | null
email: string | null
}
export type LicenseKeyCountAggregateOutputType = {
id: number
key: number
active: number
level: number
userId: number
email: number
_all: number
}
@@ -54,24 +54,24 @@ export type LicenseKeyMinAggregateInputType = {
id?: true
key?: true
active?: true
level?: true
userId?: true
email?: true
}
export type LicenseKeyMaxAggregateInputType = {
id?: true
key?: true
active?: true
level?: true
userId?: true
email?: true
}
export type LicenseKeyCountAggregateInputType = {
id?: true
key?: true
active?: true
level?: true
userId?: true
email?: true
_all?: true
}
@@ -151,8 +151,8 @@ export type LicenseKeyGroupByOutputType = {
id: string
key: string
active: boolean
level: string
userId: string
userId: string | null
email: string | null
_count: LicenseKeyCountAggregateOutputType | null
_min: LicenseKeyMinAggregateOutputType | null
_max: LicenseKeyMaxAggregateOutputType | null
@@ -180,17 +180,17 @@ export type LicenseKeyWhereInput = {
id?: Prisma.StringFilter<"LicenseKey"> | string
key?: Prisma.StringFilter<"LicenseKey"> | string
active?: Prisma.BoolFilter<"LicenseKey"> | boolean
level?: Prisma.StringFilter<"LicenseKey"> | string
userId?: Prisma.StringFilter<"LicenseKey"> | string
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
userId?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
email?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
user?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
}
export type LicenseKeyOrderByWithRelationInput = {
id?: Prisma.SortOrder
key?: Prisma.SortOrder
active?: Prisma.SortOrder
level?: Prisma.SortOrder
userId?: Prisma.SortOrder
userId?: Prisma.SortOrderInput | Prisma.SortOrder
email?: Prisma.SortOrderInput | Prisma.SortOrder
user?: Prisma.UserOrderByWithRelationInput
}
@@ -201,17 +201,17 @@ export type LicenseKeyWhereUniqueInput = Prisma.AtLeast<{
OR?: Prisma.LicenseKeyWhereInput[]
NOT?: Prisma.LicenseKeyWhereInput | Prisma.LicenseKeyWhereInput[]
active?: Prisma.BoolFilter<"LicenseKey"> | boolean
level?: Prisma.StringFilter<"LicenseKey"> | string
userId?: Prisma.StringFilter<"LicenseKey"> | string
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
userId?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
email?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
user?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
}, "id" | "key">
export type LicenseKeyOrderByWithAggregationInput = {
id?: Prisma.SortOrder
key?: Prisma.SortOrder
active?: Prisma.SortOrder
level?: Prisma.SortOrder
userId?: Prisma.SortOrder
userId?: Prisma.SortOrderInput | Prisma.SortOrder
email?: Prisma.SortOrderInput | Prisma.SortOrder
_count?: Prisma.LicenseKeyCountOrderByAggregateInput
_max?: Prisma.LicenseKeyMaxOrderByAggregateInput
_min?: Prisma.LicenseKeyMinOrderByAggregateInput
@@ -224,63 +224,63 @@ export type LicenseKeyScalarWhereWithAggregatesInput = {
id?: Prisma.StringWithAggregatesFilter<"LicenseKey"> | string
key?: Prisma.StringWithAggregatesFilter<"LicenseKey"> | string
active?: Prisma.BoolWithAggregatesFilter<"LicenseKey"> | boolean
level?: Prisma.StringWithAggregatesFilter<"LicenseKey"> | string
userId?: Prisma.StringWithAggregatesFilter<"LicenseKey"> | string
userId?: Prisma.StringNullableWithAggregatesFilter<"LicenseKey"> | string | null
email?: Prisma.StringNullableWithAggregatesFilter<"LicenseKey"> | string | null
}
export type LicenseKeyCreateInput = {
id?: string
key: string
active: boolean
level: string
user: Prisma.UserCreateNestedOneWithoutLicenseKeysInput
email?: string | null
user?: Prisma.UserCreateNestedOneWithoutLicenseKeysInput
}
export type LicenseKeyUncheckedCreateInput = {
id?: string
key: string
active: boolean
level: string
userId: string
userId?: string | null
email?: string | null
}
export type LicenseKeyUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string
user?: Prisma.UserUpdateOneRequiredWithoutLicenseKeysNestedInput
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
user?: Prisma.UserUpdateOneWithoutLicenseKeysNestedInput
}
export type LicenseKeyUncheckedUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string
userId?: Prisma.StringFieldUpdateOperationsInput | string
userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type LicenseKeyCreateManyInput = {
id?: string
key: string
active: boolean
level: string
userId: string
userId?: string | null
email?: string | null
}
export type LicenseKeyUpdateManyMutationInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type LicenseKeyUncheckedUpdateManyInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string
userId?: Prisma.StringFieldUpdateOperationsInput | string
userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type LicenseKeyListRelationFilter = {
@@ -297,24 +297,24 @@ export type LicenseKeyCountOrderByAggregateInput = {
id?: Prisma.SortOrder
key?: Prisma.SortOrder
active?: Prisma.SortOrder
level?: Prisma.SortOrder
userId?: Prisma.SortOrder
email?: Prisma.SortOrder
}
export type LicenseKeyMaxOrderByAggregateInput = {
id?: Prisma.SortOrder
key?: Prisma.SortOrder
active?: Prisma.SortOrder
level?: Prisma.SortOrder
userId?: Prisma.SortOrder
email?: Prisma.SortOrder
}
export type LicenseKeyMinOrderByAggregateInput = {
id?: Prisma.SortOrder
key?: Prisma.SortOrder
active?: Prisma.SortOrder
level?: Prisma.SortOrder
userId?: Prisma.SortOrder
email?: Prisma.SortOrder
}
export type LicenseKeyCreateNestedManyWithoutUserInput = {
@@ -363,14 +363,14 @@ export type LicenseKeyCreateWithoutUserInput = {
id?: string
key: string
active: boolean
level: string
email?: string | null
}
export type LicenseKeyUncheckedCreateWithoutUserInput = {
id?: string
key: string
active: boolean
level: string
email?: string | null
}
export type LicenseKeyCreateOrConnectWithoutUserInput = {
@@ -406,36 +406,36 @@ export type LicenseKeyScalarWhereInput = {
id?: Prisma.StringFilter<"LicenseKey"> | string
key?: Prisma.StringFilter<"LicenseKey"> | string
active?: Prisma.BoolFilter<"LicenseKey"> | boolean
level?: Prisma.StringFilter<"LicenseKey"> | string
userId?: Prisma.StringFilter<"LicenseKey"> | string
userId?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
email?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
}
export type LicenseKeyCreateManyUserInput = {
id?: string
key: string
active: boolean
level: string
email?: string | null
}
export type LicenseKeyUpdateWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type LicenseKeyUncheckedUpdateWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type LicenseKeyUncheckedUpdateManyWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
@@ -444,59 +444,59 @@ export type LicenseKeySelect<ExtArgs extends runtime.Types.Extensions.InternalAr
id?: boolean
key?: boolean
active?: boolean
level?: boolean
userId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
email?: boolean
user?: boolean | Prisma.LicenseKey$userArgs<ExtArgs>
}, ExtArgs["result"]["licenseKey"]>
export type LicenseKeySelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
key?: boolean
active?: boolean
level?: boolean
userId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
email?: boolean
user?: boolean | Prisma.LicenseKey$userArgs<ExtArgs>
}, ExtArgs["result"]["licenseKey"]>
export type LicenseKeySelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
key?: boolean
active?: boolean
level?: boolean
userId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
email?: boolean
user?: boolean | Prisma.LicenseKey$userArgs<ExtArgs>
}, ExtArgs["result"]["licenseKey"]>
export type LicenseKeySelectScalar = {
id?: boolean
key?: boolean
active?: boolean
level?: boolean
userId?: boolean
email?: boolean
}
export type LicenseKeyOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "key" | "active" | "level" | "userId", ExtArgs["result"]["licenseKey"]>
export type LicenseKeyOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "key" | "active" | "userId" | "email", ExtArgs["result"]["licenseKey"]>
export type LicenseKeyInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
user?: boolean | Prisma.LicenseKey$userArgs<ExtArgs>
}
export type LicenseKeyIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
user?: boolean | Prisma.LicenseKey$userArgs<ExtArgs>
}
export type LicenseKeyIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
user?: boolean | Prisma.LicenseKey$userArgs<ExtArgs>
}
export type $LicenseKeyPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "LicenseKey"
objects: {
user: Prisma.$UserPayload<ExtArgs>
user: Prisma.$UserPayload<ExtArgs> | null
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string
key: string
active: boolean
level: string
userId: string
userId: string | null
email: string | null
}, ExtArgs["result"]["licenseKey"]>
composites: {}
}
@@ -891,7 +891,7 @@ readonly fields: LicenseKeyFieldRefs;
*/
export interface Prisma__LicenseKeyClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
readonly [Symbol.toStringTag]: "PrismaPromise"
user<T extends Prisma.UserDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.UserDefaultArgs<ExtArgs>>): Prisma.Prisma__UserClient<runtime.Types.Result.GetResult<Prisma.$UserPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
user<T extends Prisma.LicenseKey$userArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.LicenseKey$userArgs<ExtArgs>>): Prisma.Prisma__UserClient<runtime.Types.Result.GetResult<Prisma.$UserPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
@@ -924,8 +924,8 @@ export interface LicenseKeyFieldRefs {
readonly id: Prisma.FieldRef<"LicenseKey", 'String'>
readonly key: Prisma.FieldRef<"LicenseKey", 'String'>
readonly active: Prisma.FieldRef<"LicenseKey", 'Boolean'>
readonly level: Prisma.FieldRef<"LicenseKey", 'String'>
readonly userId: Prisma.FieldRef<"LicenseKey", 'String'>
readonly email: Prisma.FieldRef<"LicenseKey", 'String'>
}
@@ -1326,6 +1326,25 @@ export type LicenseKeyDeleteManyArgs<ExtArgs extends runtime.Types.Extensions.In
limit?: number
}
/**
* LicenseKey.user
*/
export type LicenseKey$userArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the User
*/
select?: Prisma.UserSelect<ExtArgs> | null
/**
* Omit specific fields from the User
*/
omit?: Prisma.UserOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.UserInclude<ExtArgs> | null
where?: Prisma.UserWhereInput
}
/**
* LicenseKey without action
*/
+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[]
}
/**
+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;
+2 -2
View File
@@ -20,8 +20,8 @@ vi.mock('$lib/db', () => {
userRole: mkdel(),
proclamation: mkdel(),
proclamationCategory: mkdel(),
solfeggioFrequency: mkdel(),
subscriptionLevel: mkdel()
proclamationGroup: mkdel(),
solfeggioFrequency: mkdel()
}
};
});
+1 -1
View File
@@ -12,8 +12,8 @@ const modelSchemas: Record<string, z.ZodObject<any>> = {
UserRole: Schemas.UserRole,
Proclamation: Schemas.Proclamation,
ProclamationCategory: Schemas.ProclamationCategory,
ProclamationGroup: Schemas.ProclamationGroup,
SolfeggioFrequency: Schemas.SolfeggioFrequency,
SubscriptionLevel: Schemas.SubscriptionLevel,
LicenseKey: Schemas.LicenseKey
};
+35 -64
View File
@@ -9,7 +9,7 @@ const models: AdminModelConfig[] = [
displayNameSingular: 'User',
listOnly: true,
idType: 'String',
include: { role: true, profile: true, licenseKeys: true },
include: { role: true, profiles: true, licenseKeys: true },
listFields: ['name', 'email', 'emailVerified', 'role.name', 'createdAt'],
searchFields: ['name', 'email'],
formFields: ['roleId'],
@@ -27,9 +27,9 @@ const models: AdminModelConfig[] = [
},
relatedRecords: [
{
relationKey: 'profile',
label: 'Profile',
type: 'has-one',
relationKey: 'profiles',
label: 'Profiles',
type: 'has-many',
displayFields: ['firstName', 'lastName', 'phoneNumber'],
editSlug: 'profiles'
},
@@ -37,7 +37,7 @@ const models: AdminModelConfig[] = [
relationKey: 'licenseKeys',
label: 'License Keys',
type: 'has-many',
displayFields: ['key', 'active', 'level']
displayFields: ['key', 'active']
}
],
accessFilter: (viewerRoleId: number) => {
@@ -55,12 +55,13 @@ const models: AdminModelConfig[] = [
displayNameSingular: 'Profile',
idType: 'String',
include: { user: true, addresses: true },
listFields: ['firstName', 'lastName', 'phoneNumber', 'user.name'],
searchFields: ['firstName', 'lastName', 'phoneNumber'],
formFields: ['userId', 'firstName', 'lastName', 'phoneNumber'],
listFields: ['firstName', 'lastName', 'email', 'phoneNumber', 'user.name'],
searchFields: ['firstName', 'lastName', 'email', 'phoneNumber'],
formFields: ['userId', 'email', 'firstName', 'lastName', 'phoneNumber'],
readonlyFields: ['id'],
fieldTypes: {
userId: 'String',
email: 'String',
firstName: 'String',
lastName: 'String',
phoneNumber: 'String'
@@ -97,31 +98,17 @@ const models: AdminModelConfig[] = [
displayName: 'Proclamations',
displayNameSingular: 'Proclamation',
idType: 'String',
include: {
solfeggioFrequency: true,
category: { include: { parent: true } },
subscriptionLevel: true
},
listFields: ['name', 'category.name', 'solfeggioFrequency.name', 'subscriptionLevel.name'],
include: { solfeggioFrequency: true, category: true, group: true },
listFields: ['name', 'category.name', 'group.name', 'solfeggioFrequency.name'],
searchFields: ['name', 'content'],
formFields: [
'name',
'solfeggioFrequencyId',
'parentCategoryId',
'categoryId',
'subscriptionLevelId',
'content'
],
formFields: ['name', 'solfeggioFrequencyId', 'categoryId', 'groupId', 'content'],
readonlyFields: ['id'],
dataOnlyFields: ['parentCategoryId'],
cascadeFields: { categoryId: { parentField: 'parentCategoryId' } },
fieldTypes: {
name: 'String',
content: 'Tiptap',
solfeggioFrequencyId: 'Int',
parentCategoryId: 'Int',
categoryId: 'Int',
subscriptionLevelId: 'Int'
groupId: 'Int'
},
relationOptions: {
solfeggioFrequencyId: {
@@ -129,18 +116,13 @@ const models: AdminModelConfig[] = [
labelField: 'name',
valueField: 'id'
},
parentCategoryId: {
model: 'ProclamationCategory',
labelField: 'name',
valueField: 'id'
},
categoryId: {
model: 'ProclamationCategory',
labelField: 'name',
valueField: 'id'
},
subscriptionLevelId: {
model: 'SubscriptionLevel',
groupId: {
model: 'ProclamationGroup',
labelField: 'name',
valueField: 'id'
}
@@ -172,19 +154,19 @@ const models: AdminModelConfig[] = [
]
},
{
slug: 'subscription-levels',
modelName: 'SubscriptionLevel',
displayName: 'Subscription Levels',
displayNameSingular: 'Subscription Level',
slug: 'proclamation-categories',
modelName: 'ProclamationCategory',
displayName: 'Proclamation Categories',
displayNameSingular: 'Proclamation Category',
idType: 'Int',
include: { proclamations: true },
listFields: ['id', 'name', 'description'],
searchFields: ['name', 'description'],
formFields: ['name', 'description'],
listFields: ['name'],
searchFields: ['name'],
formFields: ['name'],
readonlyFields: ['id'],
fieldTypes: {
name: 'String',
description: 'String'
sortOrder: 'Int'
},
relatedRecords: [
{
@@ -197,24 +179,20 @@ const models: AdminModelConfig[] = [
]
},
{
slug: 'proclamation-categories',
modelName: 'ProclamationCategory',
displayName: 'Proclamation Categories',
displayNameSingular: 'Proclamation Category',
slug: 'proclamation-groups',
modelName: 'ProclamationGroup',
displayName: 'Proclamation Groups',
displayNameSingular: 'Proclamation Group',
idType: 'Int',
include: { proclamations: true, children: true },
listFields: ['name'],
include: { proclamations: true },
listFields: ['name', 'sortOrder'],
searchFields: ['name'],
formFields: ['name', 'parentId'],
formFields: ['name'],
readonlyFields: ['id'],
fieldTypes: {
name: 'String',
parentId: 'Int',
sortOrder: 'Int'
},
relationOptions: {
parentId: { model: 'ProclamationCategory', labelField: 'name', valueField: 'id' }
},
relatedRecords: [
{
relationKey: 'proclamations',
@@ -222,13 +200,6 @@ const models: AdminModelConfig[] = [
type: 'has-many',
displayFields: ['name'],
editSlug: 'proclamations'
},
{
relationKey: 'children',
label: 'Sub-categories',
type: 'has-many',
displayFields: ['name'],
editSlug: 'proclamation-categories'
}
]
},
@@ -239,15 +210,15 @@ const models: AdminModelConfig[] = [
displayNameSingular: 'License Key',
idType: 'String',
include: { user: true },
listFields: ['key', 'active', 'level', 'user.name'],
searchFields: ['key', 'level'],
formFields: ['active', 'level', 'userId'],
listFields: ['key', 'active', 'email', 'user.name'],
searchFields: ['key', 'email'],
formFields: ['active', 'userId', 'email'],
readonlyFields: ['id'],
fieldTypes: {
key: 'String',
active: 'Boolean',
level: 'String',
userId: 'String'
userId: 'String',
email: 'String'
},
relationOptions: {
userId: { model: 'User', labelField: 'name', valueField: 'id' }
+27 -19
View File
@@ -17,12 +17,18 @@ export const User = z.object({
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 Profile = z
.object({
userId: z.string().optional(),
email: z.string().email('Invalid email address').optional(),
firstName: name,
lastName: name,
phoneNumber: phone
})
.refine((p) => p.userId || p.email, {
message: 'Provide either a user or an email',
path: ['email']
});
export const Address = z.object({
profileId: z.string().min(1),
@@ -42,30 +48,32 @@ export const SolfeggioFrequency = z.object({
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 ProclamationGroup = z.object({
name: z.string().min(1, 'Required')
});
export const ProclamationCategory = z.object({
name: z.string().min(1, 'Required'),
parentId: z.coerce.number().int().optional()
name: z.string().min(1, 'Required')
});
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(),
categoryId: z.coerce.number().int(),
groupId: 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 LicenseKey = z
.object({
active: z.coerce.boolean(),
userId: z.string().optional(),
email: z.string().email('Invalid email address').optional()
})
.refine((k) => k.userId || k.email, {
message: 'Provide either a user or an email',
path: ['email']
});
export const UserUpdate = z.object({
id: z.string().min(1),
+14 -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
});
}
@@ -100,6 +102,7 @@ export function mockProclamation(overrides?: Record<string, unknown>) {
content: '<p>Test content</p>',
solfeggioFrequencyId: 1,
categoryId: 1,
groupId: 1,
price: 0.99,
solfeggioFrequency: {
id: 1,
@@ -108,9 +111,7 @@ export function mockProclamation(overrides?: Record<string, unknown>) {
},
category: {
id: 1,
name: 'Healing',
parentId: null,
parent: null
name: 'Healing'
},
...overrides
};
@@ -121,7 +122,16 @@ export function mockCategory(overrides?: Record<string, unknown>) {
return {
id: 1,
name: 'Healing',
parentId: null,
...overrides
};
}
/** Minimal proclamation group. */
export function mockGroup(overrides?: Record<string, unknown>) {
return {
id: 1,
name: 'General',
sortOrder: 0,
...overrides
};
}
@@ -8,20 +8,18 @@ export const load: PageServerLoad = async ({ locals }) => {
throw error(403, 'not authorized');
}
const user = await prisma.user.findUnique({
where: { id: locals.user.id },
include: {
profile: {
include: { addresses: true }
}
}
});
const user = await prisma.user.findUnique({ where: { id: locals.user.id } });
if (!user) {
throw error(403, 'not authorized');
}
return { user };
const profile = await prisma.profile.findFirst({
where: { userId: user.id },
include: { addresses: true }
});
return { user: { ...user, profile } };
};
export const actions: Actions = {
@@ -40,7 +38,7 @@ export const actions: Actions = {
}
try {
await prisma.profile.update({
await prisma.profile.updateMany({
where: { userId: locals.user.id },
data: { firstName, lastName, phoneNumber }
});
@@ -67,7 +65,7 @@ export const actions: Actions = {
return fail(400, { error: 'All address fields are required.', action: 'addAddress' });
}
const profile = await prisma.profile.findUnique({
const profile = await prisma.profile.findFirst({
where: { userId: locals.user.id },
select: { id: true }
});
@@ -23,67 +23,58 @@
}
</script>
<div class="flex h-full min-h-0 flex-col gap-8 p-6 dark:text-gray-100">
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">Account</h1>
<div class="mx-auto w-full max-w-3xl">
<h1 class="text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl">Account</h1>
<p class="mt-1 text-sm text-slate-500 sm:text-base">Manage your profile and addresses.</p>
<!-- Email (read-only) -->
<div>
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">Email</p>
<p class="mt-1 text-gray-900 dark:text-gray-100">{data.user.email}</p>
<div class="mt-6 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6">
<p class="text-xs tracking-wide text-slate-500 uppercase">Email</p>
<p class="mt-1 text-base break-words text-slate-900">{data.user.email}</p>
</div>
<!-- Profile Form -->
<section>
<h2 class="mb-4 text-lg font-medium text-gray-900 dark:text-gray-100">Profile</h2>
<section class="mt-6 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6">
<h2 class="text-lg font-semibold text-slate-900">Profile</h2>
{#if form?.action === 'updateProfile' && form?.success}
<div
class="mb-4 rounded-lg border border-green-200 bg-green-50 p-3 text-green-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
<div class="mt-4 rounded-lg bg-green-50 px-4 py-3 text-sm text-green-800" role="status">
Profile updated successfully.
</div>
{/if}
{#if form?.action === 'updateProfile' && form?.error}
<div
class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
<div class="mt-4 rounded-lg bg-red-50 px-4 py-3 text-sm text-red-700" role="alert">
{form.error}
</div>
{/if}
<form method="POST" action="?/updateProfile" class="flex max-w-lg flex-col gap-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label for="firstName" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
First Name
</label>
<form method="POST" action="?/updateProfile" class="mt-5 flex flex-col gap-4">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="flex flex-col gap-1.5">
<label for="firstName" class="text-sm font-medium text-slate-700"> First Name </label>
<input
type="text"
id="firstName"
name="firstName"
value={data.user.profile?.firstName ?? ''}
required
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
/>
</div>
<div>
<label for="lastName" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Last Name
</label>
<div class="flex flex-col gap-1.5">
<label for="lastName" class="text-sm font-medium text-slate-700"> Last Name </label>
<input
type="text"
id="lastName"
name="lastName"
value={data.user.profile?.lastName ?? ''}
required
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
/>
</div>
</div>
<div>
<label for="phoneNumber" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Phone Number
</label>
<div class="flex flex-col gap-1.5">
<label for="phoneNumber" class="text-sm font-medium text-slate-700"> Phone Number </label>
<input
type="tel"
id="phoneNumber"
@@ -91,13 +82,13 @@
bind:value={phoneNumber}
oninput={handlePhoneInput}
required
class="mt-1 w-full max-w-xs rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
/>
</div>
<div>
<button
type="submit"
class="rounded-lg bg-blue-600 px-4 py-2 font-medium text-white transition-colors hover:bg-blue-700 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600"
class="flex h-11 w-full items-center justify-center rounded-lg bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 sm:w-auto sm:px-8"
>
Save Profile
</button>
@@ -106,138 +97,112 @@
</section>
<!-- Addresses -->
<section>
<div class="mb-4 flex items-center justify-between">
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">Addresses</h2>
<section class="mt-6 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6">
<div class="flex items-center justify-between gap-4">
<h2 class="text-lg font-semibold text-slate-900">Addresses</h2>
<button
type="button"
class="rounded-lg bg-green-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-green-700 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600"
onclick={() => (showAddressForm = !showAddressForm)}
class="rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50"
>
{showAddressForm ? 'Cancel' : 'Add Address'}
</button>
</div>
{#if form?.action === 'addAddress' && form?.success}
<div
class="mb-4 rounded-lg border border-green-200 bg-green-50 p-3 text-green-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
<div class="mt-4 rounded-lg bg-green-50 px-4 py-3 text-sm text-green-800" role="status">
Address added successfully.
</div>
{/if}
{#if form?.action === 'addAddress' && form?.error}
<div
class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
<div class="mt-4 rounded-lg bg-red-50 px-4 py-3 text-sm text-red-700" role="alert">
{form.error}
</div>
{/if}
{#if showAddressForm}
<form
method="POST"
action="?/addAddress"
class="mb-6 max-w-lg rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800"
>
<div class="flex flex-col gap-3">
<div>
<label for="street" class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>Street</label
>
<form method="POST" action="?/addAddress" class="mt-5 flex flex-col gap-4">
<div class="flex flex-col gap-1.5">
<label for="street" class="text-sm font-medium text-slate-700">Street</label>
<input
type="text"
id="street"
name="street"
required
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
/>
</div>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="flex flex-col gap-1.5">
<label for="city" class="text-sm font-medium text-slate-700">City</label>
<input
type="text"
id="street"
name="street"
id="city"
name="city"
required
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
/>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label for="city" class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>City</label
>
<input
type="text"
id="city"
name="city"
required
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
/>
</div>
<div>
<label for="state" class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>State</label
>
<input
type="text"
id="state"
name="state"
required
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
/>
</div>
<div class="flex flex-col gap-1.5">
<label for="state" class="text-sm font-medium text-slate-700">State</label>
<input
type="text"
id="state"
name="state"
required
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
/>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label
for="postalCode"
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Postal Code
</label>
<input
type="text"
id="postalCode"
name="postalCode"
required
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
/>
</div>
<div>
<label
for="country"
class="block text-sm font-medium text-gray-700 dark:text-gray-300">Country</label
>
<input
type="text"
id="country"
name="country"
required
value="US"
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
/>
</div>
</div>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="flex flex-col gap-1.5">
<label for="postalCode" class="text-sm font-medium text-slate-700"> Postal Code </label>
<input
type="text"
id="postalCode"
name="postalCode"
required
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
/>
</div>
<div>
<button
type="submit"
class="rounded-lg bg-blue-600 px-4 py-2 font-medium text-white transition-colors hover:bg-blue-700 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600"
>
Save Address
</button>
<div class="flex flex-col gap-1.5">
<label for="country" class="text-sm font-medium text-slate-700">Country</label>
<input
type="text"
id="country"
name="country"
required
value="US"
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
/>
</div>
</div>
<div>
<button
type="submit"
class="flex h-11 w-full items-center justify-center rounded-lg bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 sm:w-auto sm:px-8"
>
Save Address
</button>
</div>
</form>
{/if}
{#if data.user.profile?.addresses && data.user.profile.addresses.length > 0}
<div class="flex flex-col gap-3">
{#each data.user.profile.addresses as address}
<div
class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-900"
>
<p class="text-sm text-gray-900 dark:text-gray-100">{address.street}</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
<div class="mt-5 grid grid-cols-1 gap-4 sm:grid-cols-2">
{#each data.user.profile.addresses as address (address.id)}
<div class="rounded-lg border border-slate-200 bg-slate-50 p-4">
<p class="text-sm font-medium text-slate-900">{address.street}</p>
<p class="mt-1 text-sm text-slate-600">
{address.city}, {address.state}
{address.postalCode}
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">{address.country}</p>
<p class="text-sm text-slate-600">{address.country}</p>
</div>
{/each}
</div>
{:else if !showAddressForm}
<p class="text-sm text-gray-500 dark:text-gray-400">No addresses on file.</p>
<p class="mt-5 text-sm text-slate-500">No addresses on file.</p>
{/if}
</section>
</div>
@@ -10,7 +10,7 @@ import {
vi.mock('$lib/db', () => ({
prisma: {
user: { findUnique: vi.fn() },
profile: { update: vi.fn(), findUnique: vi.fn() },
profile: { findFirst: vi.fn(), updateMany: vi.fn() },
address: { create: vi.fn() }
}
}));
@@ -19,8 +19,8 @@ import { prisma } from '$lib/db';
import { load, actions } from './+page.server';
const userFindUnique = prisma.user.findUnique as ReturnType<typeof vi.fn>;
const profileUpdate = prisma.profile.update as ReturnType<typeof vi.fn>;
const profileFindUnique = prisma.profile.findUnique as ReturnType<typeof vi.fn>;
const profileFindFirst = prisma.profile.findFirst as ReturnType<typeof vi.fn>;
const profileUpdateMany = prisma.profile.updateMany as ReturnType<typeof vi.fn>;
const addressCreate = prisma.address.create as ReturnType<typeof vi.fn>;
const invokeAction = (name: string, request: Request, locals: App.Locals) =>
@@ -29,8 +29,8 @@ const invokeAction = (name: string, request: Request, locals: App.Locals) =>
describe('(protected)/account page server', () => {
beforeEach(() => {
userFindUnique.mockReset();
profileUpdate.mockReset();
profileFindUnique.mockReset();
profileFindFirst.mockReset();
profileUpdateMany.mockReset();
addressCreate.mockReset();
});
@@ -51,24 +51,26 @@ describe('(protected)/account page server', () => {
});
it('returns user with profile and addresses', async () => {
const user = mockUserWithProfileAndAddresses();
const { profile, ...user } = mockUserWithProfileAndAddresses() as any;
userFindUnique.mockResolvedValue(user);
profileFindFirst.mockResolvedValue(profile);
const locals = createLocals(mockUser() as any);
const result = (await load({ locals } as any)) as any;
expect(result.user).toEqual(user);
expect(result.user).toEqual({ ...user, profile });
expect(result.user.profile.addresses).toHaveLength(1);
});
it('includes addresses in the query', async () => {
userFindUnique.mockResolvedValue(mockUserWithProfileAndAddresses());
it('loads the profile with addresses by user id', async () => {
userFindUnique.mockResolvedValue(mockUser());
profileFindFirst.mockResolvedValue({ id: 'profile-1' });
const locals = createLocals(mockUser() as any);
await load({ locals } as any);
expect(userFindUnique).toHaveBeenCalledWith({
where: { id: 'user-1' },
include: { profile: { include: { addresses: true } } }
expect(profileFindFirst).toHaveBeenCalledWith({
where: { userId: 'user-1' },
include: { addresses: true }
});
});
});
@@ -125,7 +127,7 @@ describe('(protected)/account page server', () => {
});
it('updates profile with server-formatted phone number on success', async () => {
profileUpdate.mockResolvedValue({});
profileUpdateMany.mockResolvedValue({});
const request = createFormRequest({
firstName: 'Jane',
@@ -136,14 +138,14 @@ describe('(protected)/account page server', () => {
const locals = createLocals(mockUser() as any);
const result = await invokeAction('updateProfile', request, locals);
expect(result).toEqual({ success: true, action: 'updateProfile' });
expect(profileUpdate).toHaveBeenCalledWith({
expect(profileUpdateMany).toHaveBeenCalledWith({
where: { userId: 'user-1' },
data: { firstName: 'Jane', lastName: 'Smith', phoneNumber: '8175551234' }
});
});
it('returns fail(500) when prisma update throws', async () => {
profileUpdate.mockRejectedValue(new Error('db error'));
profileUpdateMany.mockRejectedValue(new Error('db error'));
const request = createFormRequest({
firstName: 'Jane',
@@ -190,7 +192,7 @@ describe('(protected)/account page server', () => {
});
it('throws 400 when profile is not found', async () => {
profileFindUnique.mockResolvedValue(null);
profileFindFirst.mockResolvedValue(null);
const request = createFormRequest({
street: '123 Main St',
@@ -208,7 +210,7 @@ describe('(protected)/account page server', () => {
});
it('creates address linked to profile on success', async () => {
profileFindUnique.mockResolvedValue({ id: 'profile-1' });
profileFindFirst.mockResolvedValue({ id: 'profile-1' });
addressCreate.mockResolvedValue({});
const request = createFormRequest({
@@ -235,7 +237,7 @@ describe('(protected)/account page server', () => {
});
it('returns fail(500) when prisma create throws', async () => {
profileFindUnique.mockResolvedValue({ id: 'profile-1' });
profileFindFirst.mockResolvedValue({ id: 'profile-1' });
addressCreate.mockRejectedValue(new Error('db error'));
const request = createFormRequest({
@@ -42,41 +42,74 @@
</script>
<Window>
<form method="POST" class="border p-6 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100">
<p class="mx-auto my-4 text-center italic">Please finish setting up your profile</p>
<form method="POST" class="flex flex-col items-center gap-5">
<h1 class="text-center text-2xl font-bold text-slate-900">
Please finish setting up your profile
</h1>
{#if data.user?.image}
<img src={data.user?.image} class="mx-auto w-[8rem] rounded-full" alt="Profile" />
<img
src={data.user?.image}
alt="Profile"
class="h-24 w-24 rounded-full object-cover shadow-sm sm:h-28 sm:w-28"
/>
{:else}
<div class="mx-auto aspect-square w-[8rem] rounded-full bg-slate-900"></div>
<div
class="flex h-24 w-24 items-center justify-center rounded-full bg-indigo-100 text-3xl font-semibold text-indigo-700 sm:h-28 sm:w-28"
>
{data.user?.name?.charAt(0)?.toUpperCase() ?? '?'}
</div>
{/if}
<p class="mx-auto my-4 text-center">{data.user?.email}</p>
<div class="flex flex-col">
<input type="text" name="userId" value={data.user?.id} class="hidden" />
<input
type="text"
name="firstName"
placeholder="First Name"
class="my-2"
bind:value={firstName}
/>
<input
type="text"
name="lastName"
placeholder="Last Name"
class="my-2"
bind:value={lastName}
/>
<input
type="tel"
name="phoneNumber"
placeholder="Phone Number"
class="my-2"
bind:value={phoneNumber}
oninput={handlePhoneInput}
/>
<p class="text-base text-slate-900">{data.user?.email}</p>
<div class="flex w-full flex-col gap-4">
<input type="hidden" name="userId" value={data.user?.id} />
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="flex flex-col gap-1.5">
<label for="firstName" class="text-sm font-medium text-slate-700"> First Name </label>
<input
id="firstName"
type="text"
name="firstName"
placeholder="First Name"
bind:value={firstName}
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
/>
</div>
<div class="flex flex-col gap-1.5">
<label for="lastName" class="text-sm font-medium text-slate-700"> Last Name </label>
<input
id="lastName"
type="text"
name="lastName"
placeholder="Last Name"
bind:value={lastName}
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
/>
</div>
</div>
<div class="flex flex-col gap-1.5">
<label for="phoneNumber" class="text-sm font-medium text-slate-700"> Phone Number </label>
<input
id="phoneNumber"
type="tel"
name="phoneNumber"
placeholder="(555) 555-5555"
bind:value={phoneNumber}
oninput={handlePhoneInput}
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
/>
</div>
</div>
<button type="submit" class="bg-blue-300 p-2 px-4 dark:bg-gray-700 dark:text-gray-100"
>Submit</button
<button
type="submit"
class="mt-1 flex h-11 w-full items-center justify-center rounded-lg bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700"
>
Submit
</button>
</form>
</Window>
@@ -7,19 +7,16 @@ export const load: PageServerLoad = async ({ locals }) => {
throw error(403, 'not authorized');
}
const profile = await prisma.profile.findUnique({
const profile = await prisma.profile.findFirst({
where: { userId: locals.user.id },
select: { firstName: true }
});
const [keyStats, proclamationCount] = await Promise.all([
prisma.licenseKey.groupBy({
by: ['active'],
where: { userId: locals.user.id },
_count: true
}),
prisma.proclamation.count()
]);
const keyStats = await prisma.licenseKey.groupBy({
by: ['active'],
where: { userId: locals.user.id },
_count: true
});
const activeKeys = keyStats.find((s) => s.active)?._count ?? 0;
const totalKeys = keyStats.reduce((sum, s) => sum + s._count, 0);
@@ -27,7 +24,6 @@ export const load: PageServerLoad = async ({ locals }) => {
return {
firstName: profile?.firstName ?? 'User',
activeKeys,
totalKeys,
proclamationCount
totalKeys
};
};
@@ -1,42 +1,33 @@
<script lang="ts">
import { resolve } from '$app/paths';
import type { PageProps } from './$types';
let { data }: PageProps = $props();
</script>
<div class="flex h-full min-h-0 flex-col gap-6 p-6 dark:text-gray-100">
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">
<div class="mx-auto w-full max-w-6xl">
<h1 class="text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl">
Welcome, {data.firstName}!
</h1>
<p class="mt-1 text-sm text-slate-500 sm:text-base">Here's an overview of your account.</p>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div class="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
<a
href="/license-keys"
class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-900"
href={resolve('/license-keys')}
class="group flex flex-col gap-1 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition hover:border-indigo-200 hover:shadow"
>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Active License Keys</p>
<p class="mt-2 text-3xl font-bold text-gray-900 dark:text-gray-100">{data.activeKeys}</p>
<p class="mt-1 text-sm text-gray-400 dark:text-gray-500">{data.totalKeys} total</p>
<span class="text-sm font-medium text-slate-600">Active License Keys</span>
<span class="text-3xl font-bold text-indigo-600">{data.activeKeys}</span>
<span class="text-sm text-slate-500">{data.totalKeys} total</span>
</a>
<a
href="/admin/proclamations"
class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-900"
href={resolve('/account')}
class="group flex flex-col gap-1 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition hover:border-indigo-200 hover:shadow"
>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Proclamations</p>
<p class="mt-2 text-3xl font-bold text-gray-900 dark:text-gray-100">
{data.proclamationCount}
</p>
<p class="mt-1 text-sm text-gray-400 dark:text-gray-500">Browse all</p>
</a>
<a
href="/account"
class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-900"
>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Account</p>
<p class="mt-2 text-lg font-semibold text-gray-900 dark:text-gray-100">Manage Profile</p>
<p class="mt-1 text-sm text-gray-400 dark:text-gray-500">Edit your details</p>
<span class="text-sm font-medium text-slate-600">Account</span>
<span class="text-2xl font-semibold text-slate-900">Manage Profile</span>
<span class="text-sm text-slate-500">Edit your details</span>
</a>
</div>
</div>
@@ -4,24 +4,21 @@ import { createLocals, mockUser } from '$lib/test-utils';
vi.mock('$lib/db', () => ({
prisma: {
profile: { findUnique: vi.fn() },
licenseKey: { groupBy: vi.fn() },
proclamation: { count: vi.fn() }
profile: { findFirst: vi.fn() },
licenseKey: { groupBy: vi.fn() }
}
}));
import { prisma } from '$lib/db';
import { load } from './+page.server';
const profileFindUnique = prisma.profile.findUnique as ReturnType<typeof vi.fn>;
const profileFindFirst = prisma.profile.findFirst as ReturnType<typeof vi.fn>;
const licenseKeyGroupBy = prisma.licenseKey.groupBy as ReturnType<typeof vi.fn>;
const proclamationCount = prisma.proclamation.count as ReturnType<typeof vi.fn>;
describe('(protected)/dashboard page server load', () => {
beforeEach(() => {
profileFindUnique.mockReset();
profileFindFirst.mockReset();
licenseKeyGroupBy.mockReset();
proclamationCount.mockReset();
});
it('throws 403 when locals.user is undefined', async () => {
@@ -32,9 +29,8 @@ describe('(protected)/dashboard page server load', () => {
});
it('returns firstName from profile', async () => {
profileFindUnique.mockResolvedValue({ firstName: 'Jane' });
profileFindFirst.mockResolvedValue({ firstName: 'Jane' });
licenseKeyGroupBy.mockResolvedValue([]);
proclamationCount.mockResolvedValue(0);
const locals = createLocals(mockUser() as any);
const result = (await load({ locals } as any)) as any;
@@ -42,9 +38,8 @@ describe('(protected)/dashboard page server load', () => {
});
it('defaults firstName to "User" when profile is null', async () => {
profileFindUnique.mockResolvedValue(null);
profileFindFirst.mockResolvedValue(null);
licenseKeyGroupBy.mockResolvedValue([]);
proclamationCount.mockResolvedValue(0);
const locals = createLocals(mockUser() as any);
const result = (await load({ locals } as any)) as any;
@@ -52,12 +47,11 @@ describe('(protected)/dashboard page server load', () => {
});
it('computes activeKeys and totalKeys from groupBy results', async () => {
profileFindUnique.mockResolvedValue({ firstName: 'Jane' });
profileFindFirst.mockResolvedValue({ firstName: 'Jane' });
licenseKeyGroupBy.mockResolvedValue([
{ active: true, _count: 3 },
{ active: false, _count: 2 }
]);
proclamationCount.mockResolvedValue(10);
const locals = createLocals(mockUser() as any);
const result = (await load({ locals } as any)) as any;
@@ -66,23 +60,12 @@ describe('(protected)/dashboard page server load', () => {
});
it('returns 0 for activeKeys when none are active', async () => {
profileFindUnique.mockResolvedValue({ firstName: 'Jane' });
profileFindFirst.mockResolvedValue({ firstName: 'Jane' });
licenseKeyGroupBy.mockResolvedValue([{ active: false, _count: 4 }]);
proclamationCount.mockResolvedValue(0);
const locals = createLocals(mockUser() as any);
const result = (await load({ locals } as any)) as any;
expect(result.activeKeys).toBe(0);
expect(result.totalKeys).toBe(4);
});
it('returns proclamationCount', async () => {
profileFindUnique.mockResolvedValue({ firstName: 'Jane' });
licenseKeyGroupBy.mockResolvedValue([]);
proclamationCount.mockResolvedValue(42);
const locals = createLocals(mockUser() as any);
const result = (await load({ locals } as any)) as any;
expect(result.proclamationCount).toBe(42);
});
});
@@ -4,47 +4,35 @@
let { data }: PageProps = $props();
</script>
<div class="flex h-full min-h-0 flex-col gap-6 p-6 dark:text-gray-100">
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">License Keys</h1>
<div class="mx-auto w-full max-w-6xl">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl">License Keys</h1>
<p class="mt-1 text-sm text-slate-500">Your active and inactive license keys.</p>
</div>
</div>
{#if data.licenseKeys.length > 0}
<div class="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-800">
<div class="mt-6 overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
<!-- Desktop / tablet: table -->
<table class="hidden w-full text-left text-sm sm:table">
<thead
class="border-b border-slate-200 bg-slate-50 text-xs tracking-wide text-slate-500 uppercase"
>
<tr>
<th
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase dark:text-gray-400"
>
Key
</th>
<th
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase dark:text-gray-400"
>
Level
</th>
<th
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase dark:text-gray-400"
>
Status
</th>
<th class="px-6 py-3 font-semibold">Key</th>
<th class="px-6 py-3 font-semibold">Status</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-900">
{#each data.licenseKeys as key}
<tr>
<td
class="px-6 py-4 font-mono text-sm whitespace-nowrap text-gray-900 dark:text-gray-100"
>
{key.key}
</td>
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-700 dark:text-gray-300">
{key.level}
</td>
<td class="px-6 py-4 text-sm whitespace-nowrap">
<tbody class="divide-y divide-slate-100">
{#each data.licenseKeys as key (key.key)}
<tr class="hover:bg-slate-50">
<td class="px-6 py-4 font-mono text-slate-800">{key.key}</td>
<td class="px-6 py-4">
<span
class="inline-flex rounded-full px-2 text-xs leading-5 font-semibold {key.active
? 'bg-green-100 text-green-800 dark:bg-gray-700 dark:text-gray-200'
: 'bg-red-100 text-red-800 dark:bg-gray-700 dark:text-gray-200'}"
class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium {key.active
? 'bg-green-100 text-green-700'
: 'bg-slate-100 text-slate-600'}"
>
{key.active ? 'Active' : 'Inactive'}
</span>
@@ -53,10 +41,26 @@
{/each}
</tbody>
</table>
<!-- Mobile: card list -->
<ul class="divide-y divide-slate-100 sm:hidden">
{#each data.licenseKeys as key (key.key)}
<li class="flex items-center justify-between gap-4 px-4 py-4">
<span class="min-w-0 font-mono text-sm break-all text-slate-800">{key.key}</span>
<span
class="shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium {key.active
? 'bg-green-100 text-green-700'
: 'bg-slate-100 text-slate-600'}"
>
{key.active ? 'Active' : 'Inactive'}
</span>
</li>
{/each}
</ul>
</div>
{:else}
<div class="flex flex-1 items-center justify-center">
<p class="text-lg text-gray-500 dark:text-gray-400">You don't have any license keys yet.</p>
<div class="mt-6 rounded-2xl border border-dashed border-slate-300 bg-white p-8 text-center">
<p class="text-sm text-slate-600">You don't have any license keys yet.</p>
</div>
{/if}
</div>
@@ -1,56 +0,0 @@
import type { PageServerLoad } from './$types';
import { prisma } from '$lib/db';
function getCategoryDisplayName(category: Record<string, unknown> | null | undefined) {
if (!category) return '';
const name = String(category.name ?? '');
const parent = category.parent as Record<string, unknown> | undefined;
if (parent?.name) return `${String(parent.name)}/${name}`;
return name;
}
export const load: PageServerLoad = async ({ url }) => {
const search = url.searchParams.get('search') ?? '';
const categoryId = url.searchParams.get('category');
const where: Record<string, unknown> = {};
if (search) {
where.name = { contains: search, mode: 'insensitive' };
}
if (categoryId) {
where.categoryId = parseInt(categoryId, 10);
}
const [proclamations, categories] = await Promise.all([
prisma.proclamation.findMany({
where,
include: {
solfeggioFrequency: true,
category: { include: { parent: true } },
subscriptionLevel: true
},
orderBy: { name: 'asc' }
}),
prisma.proclamationCategory.findMany({ include: { parent: true }, orderBy: { name: 'asc' } })
]);
const normalizedProclamations = proclamations.map((p) => {
if (p.category) {
(p.category as Record<string, unknown>).displayName = getCategoryDisplayName(
p.category as Record<string, unknown>
);
}
return p;
});
const normalizedCategories = categories.map((c) => ({
...c,
displayName: getCategoryDisplayName(c)
}));
return {
proclamations: normalizedProclamations,
categories: normalizedCategories,
search,
categoryId: categoryId ? parseInt(categoryId, 10) : null
};
};
@@ -1,118 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import type { PageProps } from './$types';
let { data }: PageProps = $props();
function displayCategoryName(category: { name: string; displayName?: string }) {
return category.displayName ?? category.name;
}
let search = $state('');
let categoryId: number | null = $state(null);
let initialized = $state(false);
$effect(() => {
if (!initialized) {
search = data.search;
categoryId = data.categoryId;
initialized = true;
}
});
function applyFilters() {
const params = new URLSearchParams();
if (search) params.set('search', search);
if (categoryId) params.set('category', String(categoryId));
goto(`/proclamations?${params.toString()}`, { replaceState: true });
}
function clearFilters() {
search = '';
categoryId = null;
goto('/proclamations', { replaceState: true });
}
</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">Proclamations</h1>
<!-- Filters -->
<div class="flex flex-wrap items-end gap-4">
<div>
<label for="search" class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>Search</label
>
<input
type="text"
id="search"
bind:value={search}
placeholder="Search by name..."
class="mt-1 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"
onkeydown={(e) => e.key === 'Enter' && applyFilters()}
/>
</div>
<div>
<label for="category" class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>Category</label
>
<select
id="category"
bind:value={categoryId}
onchange={applyFilters}
class="mt-1 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"
>
<option value={null}>All Categories</option>
{#each data.categories as cat}
<option value={cat.id}>{displayCategoryName(cat)}</option>
{/each}
</select>
</div>
<button
type="button"
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"
onclick={applyFilters}
>
Search
</button>
{#if data.search || data.categoryId}
<button
type="button"
class="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
onclick={clearFilters}
>
Clear
</button>
{/if}
</div>
<!-- Results -->
{#if data.proclamations.length > 0}
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{#each data.proclamations as proclamation}
<a
href="/proclamations/{proclamation.id}"
class="flex flex-col rounded-lg border border-gray-200 bg-white p-5 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-900"
>
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">{proclamation.name}</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{displayCategoryName(proclamation.category)}
</p>
<div class="mt-auto flex items-center justify-between pt-4">
<span class="text-sm text-purple-600 dark:text-gray-400">
{proclamation.solfeggioFrequency.value} Hz &mdash;
{proclamation.solfeggioFrequency.name}
</span>
<span class="text-xs text-gray-500 dark:text-gray-400">
{proclamation.subscriptionLevel?.name ?? 'Standard'}
</span>
</div>
</a>
{/each}
</div>
{:else}
<div class="flex flex-1 items-center justify-center">
<p class="text-lg text-gray-500 dark:text-gray-400">No proclamations found.</p>
</div>
{/if}
</div>
@@ -1,27 +0,0 @@
import type { PageServerLoad } from './$types';
import { error } from '@sveltejs/kit';
import { prisma } from '$lib/db';
import { canAccessAdmin } from '$lib/svelte-admin/roles';
export const load: PageServerLoad = async ({ params, parent }) => {
const parentData = typeof parent === 'function' ? await parent() : {};
const userRoleId = (parentData as { userRoleId?: number }).userRoleId ?? null;
if (userRoleId != null && !canAccessAdmin(userRoleId)) {
throw error(403, 'not authorized');
}
const proclamation = await prisma.proclamation.findUnique({
where: { id: params.id },
include: {
solfeggioFrequency: true,
category: { include: { parent: true } },
subscriptionLevel: true
}
});
if (!proclamation) {
throw error(404, 'Proclamation not found');
}
return { proclamation };
};
@@ -1,53 +0,0 @@
<script lang="ts">
import type { PageProps } from './$types';
let { data }: PageProps = $props();
const categoryBreadcrumb = $derived(
data.proclamation.category.parent
? `${data.proclamation.category.parent.name} / ${data.proclamation.category.name}`
: data.proclamation.category.name
);
</script>
<div class="mx-auto flex h-full min-h-0 w-full max-w-3xl flex-col gap-6 p-6 dark:text-gray-100">
<a
href="/proclamations"
class="text-sm text-blue-600 hover:text-blue-800 hover:underline dark:text-gray-400 dark:hover:text-gray-200"
>
&larr; Back to Proclamations
</a>
<div>
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">
{data.proclamation.name}
</h1>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{categoryBreadcrumb}</p>
</div>
<div class="flex flex-wrap gap-4">
<div
class="rounded-lg border border-purple-200 bg-purple-50 px-4 py-2 dark:border-gray-700 dark:bg-gray-800"
>
<p class="text-xs font-medium text-purple-600 dark:text-gray-400">Frequency</p>
<p class="text-sm font-semibold text-purple-900 dark:text-gray-100">
{data.proclamation.solfeggioFrequency.value} Hz &mdash;
{data.proclamation.solfeggioFrequency.name}
</p>
</div>
<div
class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-2 dark:border-gray-700 dark:bg-gray-800"
>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Level</p>
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{data.proclamation.subscriptionLevel?.name ?? 'Standard'}
</p>
</div>
</div>
<div
class="prose max-w-none rounded-lg border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-900 dark:prose-invert"
>
{@html data.proclamation.content}
</div>
</div>
@@ -1,67 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { isHttpError } from '@sveltejs/kit';
import { mockProclamation } from '$lib/test-utils';
vi.mock('$lib/db', () => ({
prisma: {
proclamation: { findUnique: vi.fn() }
}
}));
import { prisma } from '$lib/db';
import { load } from './+page.server';
const findUnique = prisma.proclamation.findUnique as ReturnType<typeof vi.fn>;
describe('(protected)/admin/proclamations/[id] page server load', () => {
beforeEach(() => {
findUnique.mockReset();
});
it('throws 404 when proclamation is not found', async () => {
findUnique.mockResolvedValue(null);
const err = await Promise.resolve(load({ params: { id: 'nonexistent' } } as any)).catch(
(e: unknown) => e
);
expect(isHttpError(err, 404)).toBe(true);
});
it('returns proclamation when found', async () => {
const proclamation = mockProclamation();
findUnique.mockResolvedValue(proclamation);
const result = (await load({ params: { id: 'proc-1' } } as any)) as any;
expect(result.proclamation).toEqual(proclamation);
});
it('queries by params.id with frequency and category (including parent)', async () => {
findUnique.mockResolvedValue(mockProclamation());
await load({ params: { id: 'proc-1' } } as any);
expect(findUnique).toHaveBeenCalledWith({
where: { id: 'proc-1' },
include: {
solfeggioFrequency: true,
category: { include: { parent: true } },
subscriptionLevel: true
}
});
});
it('returns proclamation with parent category when present', async () => {
const proclamation = mockProclamation({
category: {
id: 2,
name: 'Inner Healing',
parentId: 1,
parent: { id: 1, name: 'Healing', parentId: null }
}
});
findUnique.mockResolvedValue(proclamation);
const result = (await load({ params: { id: 'proc-1' } } as any)) as any;
expect(result.proclamation.category.parent.name).toBe('Healing');
});
});
@@ -1,122 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mockProclamation, mockCategory, createUrl } from '$lib/test-utils';
vi.mock('$lib/db', () => ({
prisma: {
proclamation: { findMany: vi.fn() },
proclamationCategory: { findMany: vi.fn() }
}
}));
import { prisma } from '$lib/db';
import { load } from './+page.server';
const proclamationFindMany = prisma.proclamation.findMany as ReturnType<typeof vi.fn>;
const categoryFindMany = prisma.proclamationCategory.findMany as ReturnType<typeof vi.fn>;
describe('(protected)/admin/proclamations page server load', () => {
beforeEach(() => {
proclamationFindMany.mockReset();
categoryFindMany.mockReset();
});
it('returns proclamations and categories with no filters', async () => {
const proclamations = [mockProclamation()];
const categories = [mockCategory()];
proclamationFindMany.mockResolvedValue(proclamations);
categoryFindMany.mockResolvedValue(categories);
const url = createUrl('/admin/proclamations');
const result = (await load({ url } as any)) as any;
expect(result.proclamations).toEqual(proclamations);
expect(result.categories[0]).toEqual(
expect.objectContaining({
...categories[0],
displayName: categories[0].name
})
);
expect(result.search).toBe('');
expect(result.categoryId).toBeNull();
});
it('passes search filter to findMany when search param is present', async () => {
proclamationFindMany.mockResolvedValue([]);
categoryFindMany.mockResolvedValue([]);
const url = createUrl('/admin/proclamations', { search: 'healing' });
await load({ url } as any);
expect(proclamationFindMany).toHaveBeenCalledWith(
expect.objectContaining({
where: { name: { contains: 'healing', mode: 'insensitive' } }
})
);
});
it('returns search string from URL params', async () => {
proclamationFindMany.mockResolvedValue([]);
categoryFindMany.mockResolvedValue([]);
const url = createUrl('/admin/proclamations', { search: 'peace' });
const result = (await load({ url } as any)) as any;
expect(result.search).toBe('peace');
});
it('passes categoryId filter when category param is present', async () => {
proclamationFindMany.mockResolvedValue([]);
categoryFindMany.mockResolvedValue([]);
const url = createUrl('/admin/proclamations', { category: '3' });
await load({ url } as any);
expect(proclamationFindMany).toHaveBeenCalledWith(
expect.objectContaining({
where: { categoryId: 3 }
})
);
});
it('returns parsed categoryId from URL params', async () => {
proclamationFindMany.mockResolvedValue([]);
categoryFindMany.mockResolvedValue([]);
const url = createUrl('/admin/proclamations', { category: '5' });
const result = (await load({ url } as any)) as any;
expect(result.categoryId).toBe(5);
});
it('combines search and category filters', async () => {
proclamationFindMany.mockResolvedValue([]);
categoryFindMany.mockResolvedValue([]);
const url = createUrl('/admin/proclamations', { search: 'love', category: '2' });
await load({ url } as any);
expect(proclamationFindMany).toHaveBeenCalledWith(
expect.objectContaining({
where: {
name: { contains: 'love', mode: 'insensitive' },
categoryId: 2
}
})
);
});
it('includes solfeggioFrequency and category relations', async () => {
proclamationFindMany.mockResolvedValue([]);
categoryFindMany.mockResolvedValue([]);
const url = createUrl('/admin/proclamations');
await load({ url } as any);
expect(proclamationFindMany).toHaveBeenCalledWith(
expect.objectContaining({
include: {
solfeggioFrequency: true,
category: { include: { parent: true } },
subscriptionLevel: true
}
})
);
});
});
+2 -2
View File
@@ -9,12 +9,12 @@ export const load: LayoutServerLoad = async ({ locals, url }) => {
const user = await prisma.user.findUnique({
where: { id: locals.user.id },
select: { id: true, roleId: true, profile: { select: { id: true } } }
select: { id: true, roleId: true, profiles: { select: { id: true } } }
});
if (!user) {
throw error(403, 'not authorized');
}
if (!user.profile && !url.pathname.startsWith('/account/setup')) {
if (user.profiles.length === 0 && !url.pathname.startsWith('/account/setup')) {
// Skip redirect when already on /account/setup to avoid redirect loop
throw redirect(303, '/account/setup');
}
+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">
<main class="p-4 sm:p-6">
{@render children()}
</div>
</main>
</div>
+4 -9
View File
@@ -4,22 +4,17 @@
</script>
<Window>
<div class="flex flex-col items-center gap-4 p-6 text-center">
<h1 class="text-xl font-bold text-gray-900 dark:text-gray-100">
<div>
<h1>
Admin Error ({$page.status})
</h1>
<p class="max-w-md text-sm text-gray-600 dark:text-gray-400">
<p>
{#if $page.error?.message}
{$page.error.message}
{:else}
An unexpected error occurred while loading this admin page.
{/if}
</p>
<a
href="/admin"
class="mt-2 border border-gray-200 px-4 py-2 text-sm font-medium text-gray-900 hover:bg-gray-50 dark:border-gray-700 dark:text-gray-100 dark:hover:bg-gray-800"
>
Back to Admin
</a>
<a href="/admin"> Back to Admin </a>
</div>
</Window>
+5 -8
View File
@@ -5,15 +5,12 @@
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">
<h1>Admin Dashboard</h1>
<div>
{#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">
<a href={'/admin/' + model.slug}>
<h2>{model.displayName}</h2>
<p>
{model.listFields.length} fields
</p>
</a>
@@ -8,7 +8,6 @@
let mode = $state<'normal' | 'emergency' | null>(null);
let expiryDate = $state<string | null>(null);
let toastMessage = $state('');
let toastType = $state<'success' | 'error'>('success');
function handleRotate(emergency: boolean) {
return ({ cancel }: any) => {
@@ -29,7 +28,6 @@
}
} else {
toastMessage = 'Rotation failed';
toastType = 'error';
}
};
};
@@ -38,7 +36,6 @@
function copyKey() {
navigator.clipboard.writeText(newKey);
toastMessage = 'Copied to clipboard';
toastType = 'success';
}
function dismissKey() {
@@ -48,44 +45,26 @@
}
</script>
<div class="p-6">
<h1 class="mb-4 text-xl font-bold text-gray-900 dark:text-gray-100">API Key Management</h1>
<div>
<h1>API Key Management</h1>
{#if newKey}
<div
class="mb-6 border border-amber-300 bg-amber-50 p-4 dark:border-amber-700 dark:bg-amber-900/30"
>
<p class="mb-2 text-sm font-semibold text-amber-800 dark:text-amber-300">
<div>
<p>
{mode === 'emergency' ? 'Emergency Rotation Complete' : 'New API Key Generated'}
</p>
<p class="mb-1 text-xs text-amber-700 dark:text-amber-400">
This key will not be shown again. Copy it now.
</p>
<div class="mb-3">
<code
class="rounded border border-amber-300 bg-white px-3 py-2 font-mono text-sm break-all dark:border-amber-600 dark:bg-gray-900 dark:text-gray-100"
>
<p>This key will not be shown again. Copy it now.</p>
<div>
<code>
{newKey}
</code>
</div>
<div class="flex gap-2">
<button
type="button"
onclick={copyKey}
class="border border-amber-300 bg-white px-3 py-1.5 text-sm font-medium hover:bg-amber-100 dark:border-amber-600 dark:bg-gray-800 dark:text-amber-300 dark:hover:bg-amber-900/50"
>
Copy to Clipboard
</button>
<button
type="button"
onclick={dismissKey}
class="border border-gray-300 px-3 py-1.5 text-sm dark:border-gray-600 dark:text-gray-300"
>
Dismiss
</button>
<div>
<button type="button" onclick={copyKey}> Copy to Clipboard </button>
<button type="button" onclick={dismissKey}> Dismiss </button>
</div>
{#if mode === 'normal' && expiryDate}
<p class="mt-3 text-xs text-amber-700 dark:text-amber-400">
<p>
The previous key will remain valid until {new Date(expiryDate).toLocaleString()}
to give in-field devices time to update.
</p>
@@ -94,23 +73,17 @@
{/if}
{#if toastMessage}
<div
class="mb-4 border px-4 py-2 text-sm {toastType === 'success'
? 'border-green-300 bg-green-50 text-green-700'
: 'border-red-300 bg-red-50 text-red-700'}"
>
<div>
{toastMessage}
</div>
{/if}
<div class="mb-6 rounded border border-gray-200 p-4 dark:border-gray-700">
<h2 class="mb-3 text-sm font-semibold text-gray-700 dark:text-gray-300">Current Key Status</h2>
<div>
<h2>Current Key Status</h2>
{#if data.activeKey}
<div class="space-y-1 text-sm text-gray-600 dark:text-gray-400">
<div>
<div>
Prefix: <code class="font-mono font-medium text-gray-900 dark:text-gray-100"
>{data.activeKey.prefix}</code
>
Prefix: <code>{data.activeKey.prefix}</code>
</div>
<div>Created: {new Date(data.activeKey.createdAt).toLocaleString()}</div>
{#if data.activeKey.expiresAt}
@@ -118,31 +91,21 @@
{/if}
</div>
{:else}
<p class="text-sm text-gray-500 dark:text-gray-400">No active API key. Generate one below.</p>
<p>No active API key. Generate one below.</p>
{/if}
{#if data.totalRotated > 0}
<p class="mt-2 text-xs text-gray-400">
<p>
{data.totalRotated} previous key(s) have been rotated.
</p>
{/if}
</div>
<div class="flex flex-wrap gap-3">
<div>
<form method="POST" action="?/rotate" use:enhance={handleRotate(false)}>
<button
type="submit"
class="border border-gray-300 bg-gray-100 px-4 py-2 text-sm font-medium hover:bg-gray-200 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
Generate New API Key
</button>
<button type="submit"> Generate New API Key </button>
</form>
<form method="POST" action="?/rotateEmergency" use:enhance={handleRotate(true)}>
<button
type="submit"
class="border border-red-300 bg-red-50 px-4 py-2 text-sm font-medium text-red-700 hover:bg-red-100 dark:border-red-700 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50"
>
Emergency Rotation
</button>
<button type="submit"> Emergency Rotation </button>
</form>
</div>
</div>
@@ -10,18 +10,7 @@ const config = getModelConfig('license-keys')!;
export const load: PageServerLoad = async ({ locals }) => {
const records = await loadRecords(locals, config);
const subscriptionLevels = await prisma.subscriptionLevel.findMany({
select: { id: true, name: true },
orderBy: { name: 'asc' }
});
return {
...records,
subscriptionLevels: subscriptionLevels.map((s: { id: number; name: string }) => ({
label: s.name,
value: s.name
}))
};
return { ...records };
};
export const actions = {
@@ -29,20 +18,13 @@ export const actions = {
if (!locals.user) throw error(401, 'Unauthorized');
const formData = await request.formData();
const email = formData.get('email')?.toString();
const level = formData.get('level')?.toString();
const email = formData.get('email')?.toString()?.trim().toLowerCase();
if (!email) {
return { success: false, error: 'Email is required' };
}
if (!level) {
return { success: false, error: 'Subscription level is required' };
}
const user = await prisma.user.findUnique({ where: { email } });
if (!user) {
return { success: false, error: 'No user found with that email' };
}
const hex = crypto.randomUUID().replace(/-/g, '').slice(0, 16).toUpperCase();
const key = `${hex.slice(0, 4)}-${hex.slice(4, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}`;
@@ -51,19 +33,21 @@ export const actions = {
data: {
key,
active: true,
level,
userId: user.id
userId: user?.id,
email: user ? undefined : email
}
});
try {
const profile = await prisma.profile.findUnique({ where: { userId: user.id } });
const profile = await prisma.profile.findFirst({
where: user ? { userId: user.id } : { email }
});
await sendMail({
to: email,
subject: 'Your Rhemawave License Key',
templateId: 'license-key-delivery',
data: {
firstName: profile?.firstName ?? user.name ?? 'Valued Customer',
firstName: profile?.firstName ?? user?.name ?? 'Valued Customer',
license_key: key
}
});
@@ -92,8 +76,8 @@ export const actions = {
data: {
key,
active: true,
level: existing.level,
userId: existing.userId
userId: existing.userId,
email: existing.email
}
});
@@ -5,7 +5,6 @@
import AdminModal from '$lib/components/admin/AdminModal.svelte';
import Toast from '$lib/components/admin/Toast.svelte';
import { getModelConfig } from '$lib/svelte-admin/registry';
import LicenseKeyForm from './LicenseKeyForm.svelte';
let { data }: PageProps = $props();
@@ -15,7 +14,9 @@
let replaceOpen = $state(false);
let replaceRecord = $state<Record<string, unknown> | null>(null);
let toastMessage = $state('');
let toastType = $state<'success' | 'error'>('success');
let email = $state('');
let formError = $state('');
function openCreate() {
createOpen = true;
@@ -40,26 +41,35 @@
if (result.type === 'success' && result.status === 200) {
closeReplace();
toastMessage = 'License key replaced';
toastType = 'success';
} else {
toastMessage = 'Failed to replace license key';
toastType = 'error';
}
await update();
};
}
function createEnhance() {
return async ({ result, update }: any) => {
if (result.type === 'success' && result.status === 200) {
if (result.data?.error) {
formError = result.data.error;
} else {
email = '';
formError = '';
await update();
closeCreate();
}
} else {
await update();
}
};
}
</script>
<div class="p-6">
<div class="mb-4 flex items-center justify-between">
<h1 class="text-xl font-bold text-gray-900 dark:text-gray-100">{config.displayName}</h1>
<button
type="button"
onclick={openCreate}
class="border border-gray-300 px-3 py-1.5 text-sm font-medium dark:border-gray-600 dark:text-gray-300"
>
New License Key +
</button>
<div>
<div>
<h1>{config.displayName}</h1>
<button type="button" onclick={openCreate}>New License Key +</button>
</div>
<AdminTable
@@ -74,58 +84,45 @@
</div>
<AdminModal title="Create License Key" open={createOpen} onClose={closeCreate}>
<LicenseKeyForm subscriptionLevels={data.subscriptionLevels} onSuccess={closeCreate} />
<form method="POST" action="?/createKey" use:enhance={createEnhance}>
{#if formError}
<div>{formError}</div>
{/if}
<div>
<label for="field-email">Email</label>
<input
type="email"
id="field-email"
name="email"
required
bind:value={email}
placeholder="user@example.com"
/>
<p>If no account exists for this email yet, the key will be attached when they register.</p>
</div>
<div>
<button type="submit">Create &amp; Email Key</button>
</div>
</form>
</AdminModal>
<AdminModal title="Replace License Key" open={replaceOpen} onClose={closeReplace}>
{#if replaceRecord}
<div class="flex flex-col gap-4">
<p class="text-sm text-gray-700 dark:text-gray-300">
Generate a new key to replace <strong>{String(replaceRecord.key)}</strong>?
</p>
<div
class="rounded border border-gray-200 bg-gray-50 p-3 text-sm dark:border-gray-700 dark:bg-gray-800"
>
<div class="mb-1 text-gray-500 dark:text-gray-400">
User: <span class="font-medium text-gray-700 dark:text-gray-300"
>{String(replaceRecord['user.name'] ?? '')}</span
>
</div>
<div class="text-gray-500 dark:text-gray-400">
Level: <span class="font-medium text-gray-700 dark:text-gray-300"
>{String(replaceRecord.level ?? '')}</span
>
</div>
<div>
<p>Generate a new key to replace <strong>{String(replaceRecord.key)}</strong>?</p>
<div>
<div>User: <span>{String(replaceRecord['user.name'] ?? '')}</span></div>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400">
The existing key will be deleted and a new one with the same subscription level will be
created and emailed.
</p>
<form
method="POST"
action="?/replace"
use:enhance={replaceEnhance}
class="flex justify-end gap-2 pt-2"
>
<p>The existing key will be deleted and a new one will be created and emailed.</p>
<form method="POST" action="?/replace" use:enhance={replaceEnhance}>
<input type="hidden" name="id" value={String(replaceRecord.id)} />
<button
type="button"
onclick={closeReplace}
class="border border-gray-300 px-3 py-1.5 text-sm dark:border-gray-600 dark:text-gray-300"
>
Cancel
</button>
<button
type="submit"
class="border border-amber-300 bg-amber-50 px-4 py-1.5 text-sm font-medium hover:bg-amber-100 dark:border-amber-700 dark:bg-amber-900/30 dark:text-amber-300 dark:hover:bg-amber-900/50"
>
Generate Replacement
</button>
<button type="button" onclick={closeReplace}>Cancel</button>
<button type="submit">Generate Replacement</button>
</form>
</div>
{/if}
</AdminModal>
{#if toastMessage}
<Toast message={toastMessage} type={toastType} />
<Toast message={toastMessage} />
{/if}
@@ -1,91 +0,0 @@
<script lang="ts">
import { enhance } from '$app/forms';
let {
subscriptionLevels = [] as { label: string; value: string }[],
onSuccess
}: {
subscriptionLevels?: { label: string; value: string }[];
onSuccess?: () => void;
} = $props();
let email = $state('');
let level = $state('');
let formError = $state('');
function handleEnhance() {
return async ({ result, update }: any) => {
if (result.type === 'success' && result.status === 200) {
if (result.data?.error) {
formError = result.data.error;
} else {
email = '';
level = '';
formError = '';
await update();
onSuccess?.();
}
} else {
await update();
}
};
}
</script>
<form method="POST" action="?/createKey" use:enhance={handleEnhance}>
{#if formError}
<div class="mb-4 border border-red-300 bg-red-50 px-4 py-2 text-sm text-red-700">
{formError}
</div>
{/if}
<div class="flex flex-col gap-4">
<div>
<label
for="field-email"
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
User Email
</label>
<input
type="email"
id="field-email"
name="email"
required
bind:value={email}
class="w-full border border-gray-300 px-3 py-1.5 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
placeholder="user@example.com"
/>
</div>
<div>
<label
for="field-level"
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Subscription Level
</label>
<select
name="level"
id="field-level"
required
onchange={(e) => (level = (e.target as HTMLSelectElement).value)}
class="w-full border border-gray-300 px-3 py-1.5 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
>
<option value="" disabled selected={!level}>-- Select --</option>
{#each subscriptionLevels as opt (opt.value)}
<option value={opt.value} selected={opt.value === level}>{opt.label}</option>
{/each}
</select>
</div>
<div class="flex justify-end gap-2 pt-2">
<button
type="submit"
class="border border-gray-300 bg-gray-100 px-4 py-1.5 text-sm font-medium hover:bg-gray-200 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
Create & Email Key
</button>
</div>
</div>
</form>
@@ -19,9 +19,8 @@ vi.mock('$lib/svelte-admin/crud', () => ({
vi.mock('$lib/db', () => ({
prisma: {
user: { findUnique: vi.fn() },
profile: { findUnique: vi.fn() },
licenseKey: { create: vi.fn() },
subscriptionLevel: { findMany: vi.fn() }
profile: { findFirst: vi.fn() },
licenseKey: { create: vi.fn() }
}
}));
@@ -29,7 +28,7 @@ import { prisma } from '$lib/db';
import { actions } from './+page.server';
const mockFindUnique = prisma.user.findUnique as ReturnType<typeof vi.fn>;
const mockProfileFindUnique = prisma.profile.findUnique as ReturnType<typeof vi.fn>;
const mockProfileFindFirst = prisma.profile.findFirst as ReturnType<typeof vi.fn>;
const mockLicenseKeyCreate = prisma.licenseKey.create as ReturnType<typeof vi.fn>;
describe('admin license-keys createKey action', () => {
@@ -39,7 +38,7 @@ describe('admin license-keys createKey action', () => {
it('rejects unauthenticated requests', async () => {
const locals = createLocals();
const req = createFormRequest({ email: 'jane@example.com', level: 'standard' });
const req = createFormRequest({ email: 'jane@example.com' });
const err = await actions.createKey({ request: req, locals } as any).catch((e) => e);
expect(isHttpError(err, 401)).toBe(true);
@@ -47,27 +46,36 @@ describe('admin license-keys createKey action', () => {
it('returns error when email is missing', async () => {
const locals = createLocals(mockUser() as any);
const req = createFormRequest({ level: 'standard' });
const req = createFormRequest({});
const result = await actions.createKey({ request: req, locals } as any);
expect(result).toEqual({ success: false, error: 'Email is required' });
});
it('returns error when level is missing', async () => {
const locals = createLocals(mockUser() as any);
const req = createFormRequest({ email: 'jane@example.com' });
const result = await actions.createKey({ request: req, locals } as any);
expect(result).toEqual({ success: false, error: 'Subscription level is required' });
});
it('returns error when user is not found', async () => {
it('creates a key with only an email when no user exists yet', async () => {
mockFindUnique.mockResolvedValue(null);
mockProfileFindFirst.mockResolvedValue(null);
mockLicenseKeyCreate.mockResolvedValue({});
sendMail.mockResolvedValue({ id: 'email-1' });
const locals = createLocals(mockUser() as any);
const req = createFormRequest({ email: 'nobody@example.com', level: 'standard' });
const req = createFormRequest({ email: 'nobody@example.com' });
const result = await actions.createKey({ request: req, locals } as any);
expect(result).toEqual({ success: false, error: 'No user found with that email' });
expect(result).toEqual({ success: true, error: undefined });
expect(mockLicenseKeyCreate).toHaveBeenCalledOnce();
const createCall = mockLicenseKeyCreate.mock.calls[0][0];
expect(createCall.data.active).toBe(true);
expect(createCall.data.userId).toBeUndefined();
expect(createCall.data.email).toBe('nobody@example.com');
expect(sendMail).toHaveBeenCalledOnce();
const mailCall = sendMail.mock.calls[0][0];
expect(mailCall.to).toBe('nobody@example.com');
expect(mailCall.templateId).toBe('license-key-delivery');
expect(mailCall.data.firstName).toBe('Valued Customer');
});
it('creates a license key and sends email with license_key variable', async () => {
@@ -75,12 +83,12 @@ describe('admin license-keys createKey action', () => {
const profile = { firstName: 'Jane' };
mockFindUnique.mockResolvedValue(user);
mockProfileFindUnique.mockResolvedValue(profile);
mockProfileFindFirst.mockResolvedValue(profile);
mockLicenseKeyCreate.mockResolvedValue({});
sendMail.mockResolvedValue({ id: 'email-1' });
const locals = createLocals(user as any);
const req = createFormRequest({ email: user.email, level: 'standard' });
const req = createFormRequest({ email: user.email });
const result = await actions.createKey({ request: req, locals } as any);
@@ -90,7 +98,6 @@ describe('admin license-keys createKey action', () => {
const createCall = mockLicenseKeyCreate.mock.calls[0][0];
expect(createCall.data.key).toMatch(/^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/);
expect(createCall.data.active).toBe(true);
expect(createCall.data.level).toBe('standard');
expect(createCall.data.userId).toBe('user-1');
expect(sendMail).toHaveBeenCalledOnce();
@@ -106,12 +113,12 @@ describe('admin license-keys createKey action', () => {
const user = mockUser();
mockFindUnique.mockResolvedValue(user);
mockProfileFindUnique.mockResolvedValue(null);
mockProfileFindFirst.mockResolvedValue(null);
mockLicenseKeyCreate.mockResolvedValue({});
sendMail.mockResolvedValue({ id: 'email-1' });
const locals = createLocals(user as any);
const req = createFormRequest({ email: user.email, level: 'premium' });
const req = createFormRequest({ email: user.email });
await actions.createKey({ request: req, locals } as any);
@@ -125,14 +132,14 @@ describe('admin license-keys createKey action', () => {
const user = mockUser();
mockFindUnique.mockResolvedValue(user);
mockProfileFindUnique.mockResolvedValue({ firstName: 'Jane' });
mockProfileFindFirst.mockResolvedValue({ firstName: 'Jane' });
mockLicenseKeyCreate.mockResolvedValue({});
sendMail.mockRejectedValue(new Error('Resend API down'));
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const locals = createLocals(user as any);
const req = createFormRequest({ email: user.email, level: 'standard' });
const req = createFormRequest({ email: user.email });
const result = await actions.createKey({ request: req, locals } as any);
expect(result).toEqual({ success: true, error: undefined });
@@ -4,51 +4,25 @@ import { getDelegate } from '$lib/svelte-admin/prisma-delegate';
const config = getModelConfig('proclamation-categories')!;
export const load: PageServerLoad = async ({ locals }) => {
export const load: PageServerLoad = async () => {
const delegate = getDelegate(config);
const records = await delegate.findMany({
where: { parentId: null },
include: config.include,
orderBy: { sortOrder: 'asc' }
orderBy: [{ sortOrder: 'asc' }, { name: 'asc' }]
});
const relationOptionsData: Record<string, { label: string; value: string | number }[]> = {};
for (const [field, relConfig] of Object.entries(config.relationOptions ?? {})) {
const relDelegate = getDelegate({ ...config, modelName: relConfig.model });
const selectFields: Record<string, boolean> = {
[relConfig.valueField]: true,
[relConfig.labelField]: true
};
const options = await relDelegate.findMany({
select: selectFields,
orderBy: { [relConfig.labelField]: 'asc' }
});
const mapped = (options as Record<string, unknown>[]).map((opt: Record<string, unknown>) => ({
label: String(opt[relConfig.labelField]),
value: opt[relConfig.valueField] as string | number
}));
relationOptionsData[field] = mapped;
}
return {
records: records as Record<string, unknown>[],
relationOptionsData
relationOptionsData: {}
};
};
export const actions = {
upsert: async ({ request, locals }) => {
upsert: async ({ request }) => {
const formData = await request.formData();
const name = (formData.get('name')?.toString() ?? '').trim();
const rawId = formData.get('id')?.toString();
const childIds = formData.getAll('childId').map((v) => v.toString());
const childNames = formData
.getAll('childName')
.map((v) => v.toString().trim())
.filter(Boolean);
if (!name) {
return { success: false, error: 'Name is required', fieldErrors: { name: 'Required' } };
@@ -57,70 +31,31 @@ export const actions = {
const delegate = getDelegate(config);
try {
let categoryId: number;
if (rawId) {
const id = parseInt(rawId, 10);
await delegate.update({
where: { id },
where: { id: parseInt(rawId, 10) },
data: { name }
});
categoryId = id;
} else {
const maxSort = await delegate.findFirst({
where: { parentId: null },
orderBy: { sortOrder: 'desc' },
select: { sortOrder: true }
});
const sortOrder = (maxSort?.sortOrder ?? -1) + 1;
const created = await delegate.create({
data: { name, sortOrder }
await delegate.create({
data: { name, sortOrder: (maxSort?.sortOrder ?? -1) + 1 }
});
categoryId = created.id;
}
const existingChildren = await delegate.findMany({
where: { parentId: categoryId },
select: { id: true }
});
const existingChildIdSet = new Set(existingChildren.map((c: { id: number }) => c.id));
const submittedIdSet = new Set<number>();
for (let i = 0; i < childNames.length; i++) {
const childName = childNames[i];
if (!childName) continue;
const childIdRaw = childIds[i] ? parseInt(childIds[i], 10) : null;
if (childIdRaw && existingChildIdSet.has(childIdRaw)) {
submittedIdSet.add(childIdRaw);
await delegate.update({
where: { id: childIdRaw },
data: { name: childName }
});
} else {
await delegate.create({
data: { name: childName, parentId: categoryId }
});
}
}
for (const child of existingChildren) {
if (!submittedIdSet.has(child.id)) {
await delegate.delete({ where: { id: child.id } });
}
}
return { success: true };
} catch (e: unknown) {
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2002') {
return {
success: false,
error: 'A sub-category with this name already exists under this parent.'
error: 'A category with this name already exists.'
};
}
throw e;
}
return { success: true };
},
delete: async ({ request, locals }) => {
const formData = await request.formData();
@@ -137,8 +72,17 @@ export const actions = {
if (!match) return { success: false, error: 'Access denied' };
}
await delegate.deleteMany({ where: { parentId: id } });
await delegate.delete({ where: { id } });
try {
await delegate.delete({ where: { id } });
} catch (e: unknown) {
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2003') {
return {
success: false,
error: 'Cannot delete: one or more proclamations use this category.'
};
}
throw e;
}
return { success: true };
},
@@ -1,12 +1,11 @@
<script lang="ts">
import type { PageProps } from './$types';
import type { ActionResult } from '$lib/svelte-admin/types';
import { enhance } from '$app/forms';
import { invalidateAll } from '$app/navigation';
import AdminTable from '$lib/components/admin/AdminTable.svelte';
import AdminModal from '$lib/components/admin/AdminModal.svelte';
import Toast from '$lib/components/admin/Toast.svelte';
import { getModelConfig } from '$lib/svelte-admin/registry';
import { invalidateAll } from '$app/navigation';
import CategoryForm from './CategoryForm.svelte';
let { data, form }: PageProps = $props();
@@ -16,16 +15,17 @@
let modalOpen = $state(false);
let editingRecord = $state<Record<string, unknown> | null>(null);
let toastMessage = $state('');
let toastType = $state<'success' | 'error'>('success');
let fieldErrors = $state<Record<string, string>>({});
let formError = $state('');
let name = $state('');
let dirty = $state(false);
$effect(() => {
if (form && 'success' in (form as any)) {
const result = form as any;
if (result.success) {
toastMessage = 'Saved successfully';
toastType = 'success';
closeModal();
} else {
formError = result.error ?? '';
@@ -34,6 +34,11 @@
}
});
$effect(() => {
name = String((editingRecord ?? {})['name'] ?? '');
dirty = false;
});
async function handleReorder(ids: (string | number)[]) {
const formData = new FormData();
formData.set('ids', JSON.stringify(ids));
@@ -61,10 +66,14 @@
formError = '';
fieldErrors = {};
}
function markDirty() {
if (editingRecord?.id) dirty = true;
}
</script>
<div class="p-6">
<h1 class="mb-4 text-xl font-bold text-gray-900 dark:text-gray-100">{config.displayName}</h1>
<div>
<h1>{config.displayName}</h1>
<AdminTable
records={data.records}
listFields={config.listFields}
@@ -87,14 +96,33 @@
{canDelete}
recordId={editingRecord?.id as string | undefined}
>
<CategoryForm
record={editingRecord ?? {}}
{fieldErrors}
{formError}
submitLabel={editingRecord ? 'Save' : 'Create'}
/>
<form action="?/upsert" method="POST" use:enhance oninput={markDirty}>
{#if editingRecord?.id}
<input type="hidden" name="id" value={String(editingRecord.id)} />
{/if}
{#if formError}
<div>{formError}</div>
{/if}
<div>
<div>
<label for="field-name">Name</label>
<input type="text" id="field-name" name="name" bind:value={name} />
{#if fieldErrors.name}
<p>{fieldErrors.name}</p>
{/if}
</div>
<div>
<button type="submit" disabled={!!editingRecord?.id && !dirty}>
{editingRecord ? 'Save' : 'Create'}
</button>
</div>
</div>
</form>
</AdminModal>
{#if toastMessage}
<Toast message={toastMessage} type={toastType} />
<Toast message={toastMessage} />
{/if}
@@ -1,124 +0,0 @@
<script lang="ts">
import { enhance } from '$app/forms';
let {
record = {} as Record<string, unknown>,
fieldErrors = {} as Record<string, string>,
formError = '',
action = '?/upsert',
submitLabel = 'Save'
}: {
record?: Record<string, unknown>;
fieldErrors?: Record<string, string>;
formError?: string;
action?: string;
submitLabel?: string;
} = $props();
let name = $state('');
let children = $state<{ id: number | null; name: string }[]>([]);
let dirty = $state(false);
$effect(() => {
dirty = false;
});
$effect(() => {
name = String(record.name ?? '');
const existingChildren = (record.children as Array<{ id: number; name: string }>) ?? [];
children = existingChildren.map((c) => ({ id: c.id, name: c.name }));
});
function markDirty() {
if (record.id) dirty = true;
}
function addChild() {
children = [...children, { id: null, name: '' }];
if (record.id) dirty = true;
}
function removeChild(index: number) {
children = children.filter((_, i) => i !== index);
if (record.id) dirty = true;
}
</script>
<form {action} method="POST" use:enhance oninput={markDirty}>
{#if record?.id}
<input type="hidden" name="id" value={String(record.id)} />
{/if}
{#if formError}
<div class="mb-4 border border-red-300 bg-red-50 px-4 py-2 text-sm text-red-700">
{formError}
</div>
{/if}
<div class="flex flex-col gap-4">
<div>
<label
for="field-name"
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Name
</label>
<input
type="text"
id="field-name"
name="name"
bind:value={name}
class="w-full border px-3 py-1.5 text-sm {fieldErrors.name
? 'border-red-400'
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
/>
{#if fieldErrors.name}
<p class="mt-1 text-xs text-red-600">{fieldErrors.name}</p>
{/if}
</div>
<div>
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
Sub-categories
</label>
{#each children as child, i}
<div class="mb-2 flex items-center gap-2">
<input type="hidden" name="childId" value={child.id ?? ''} />
<input
type="text"
name="childName"
bind:value={child.name}
placeholder="Sub-category name"
class="flex-1 border px-3 py-1.5 text-sm {fieldErrors[`childName.${i}`]
? 'border-red-400'
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
/>
<button
type="button"
onclick={() => removeChild(i)}
class="shrink-0 px-2 py-1 text-sm text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
>
Remove
</button>
</div>
{/each}
<button
type="button"
onclick={addChild}
class="mt-1 text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
>
+ Add Sub-category
</button>
</div>
<div class="flex justify-end gap-2 pt-2">
<button
type="submit"
disabled={!!record?.id && !dirty}
class="border border-gray-300 bg-gray-100 px-4 py-1.5 text-sm font-medium hover:bg-gray-200 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
{submitLabel}
</button>
</div>
</div>
</form>
@@ -0,0 +1,107 @@
import type { PageServerLoad } from './$types';
import { getModelConfig } from '$lib/svelte-admin/registry';
import { getDelegate } from '$lib/svelte-admin/prisma-delegate';
const config = getModelConfig('proclamation-groups')!;
export const load: PageServerLoad = async () => {
const delegate = getDelegate(config);
const records = await delegate.findMany({
orderBy: [{ sortOrder: 'asc' }, { name: 'asc' }]
});
return {
records: records as Record<string, unknown>[],
relationOptionsData: {}
};
};
export const actions = {
upsert: async ({ request }) => {
const formData = await request.formData();
const name = (formData.get('name')?.toString() ?? '').trim();
const rawId = formData.get('id')?.toString();
if (!name) {
return { success: false, error: 'Name is required', fieldErrors: { name: 'Required' } };
}
const delegate = getDelegate(config);
try {
if (rawId) {
await delegate.update({
where: { id: parseInt(rawId, 10) },
data: { name }
});
} else {
const maxSort = await delegate.findFirst({
orderBy: { sortOrder: 'desc' },
select: { sortOrder: true }
});
await delegate.create({
data: { name, sortOrder: (maxSort?.sortOrder ?? -1) + 1 }
});
}
} catch (e: unknown) {
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2002') {
return {
success: false,
error: 'A group with this name already exists.'
};
}
throw e;
}
return { success: true };
},
delete: async ({ request, locals }) => {
const formData = await request.formData();
const rawId = formData.get('id')?.toString();
if (!rawId) return { success: false, error: 'Missing id' };
const id = parseInt(rawId, 10);
const delegate = getDelegate(config);
const viewerRoleId = (locals.user as { roleId?: number } | null)?.roleId ?? 4;
const where = config.accessFilter?.(viewerRoleId);
if (where) {
const match = await delegate.findFirst({ where: { id, ...where } });
if (!match) return { success: false, error: 'Access denied' };
}
try {
await delegate.delete({ where: { id } });
} catch (e: unknown) {
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2003') {
return {
success: false,
error: 'Cannot delete: one or more proclamations use this group.'
};
}
throw e;
}
return { success: true };
},
reorder: async ({ request }) => {
const formData = await request.formData();
const raw = formData.get('ids')?.toString();
if (!raw) return { success: false, error: 'Missing ids' };
const ids = JSON.parse(raw) as number[];
const delegate = getDelegate(config);
for (let i = 0; i < ids.length; i++) {
await delegate.update({
where: { id: ids[i] },
data: { sortOrder: i }
});
}
return { success: true };
}
};
@@ -1,30 +1,31 @@
<script lang="ts">
import type { PageProps } from './$types';
import type { ActionResult } from '$lib/svelte-admin/types';
import { enhance } from '$app/forms';
import { invalidateAll } from '$app/navigation';
import AdminTable from '$lib/components/admin/AdminTable.svelte';
import AdminModal from '$lib/components/admin/AdminModal.svelte';
import Toast from '$lib/components/admin/Toast.svelte';
import { getModelConfig } from '$lib/svelte-admin/registry';
import LevelForm from './LevelForm.svelte';
let { data, form }: PageProps = $props();
const config = getModelConfig('subscription-levels')!;
const config = getModelConfig('proclamation-groups')!;
const canDelete = $derived((data.adminUser?.roleId ?? 4) <= 2);
let modalOpen = $state(false);
let editingRecord = $state<Record<string, unknown> | null>(null);
let toastMessage = $state('');
let toastType = $state<'success' | 'error'>('success');
let fieldErrors = $state<Record<string, string>>({});
let formError = $state('');
let name = $state('');
let dirty = $state(false);
$effect(() => {
if (form && 'success' in (form as any)) {
const result = form as any;
if (result.success) {
toastMessage = 'Saved successfully';
toastType = 'success';
closeModal();
} else {
formError = result.error ?? '';
@@ -33,6 +34,18 @@
}
});
$effect(() => {
name = String((editingRecord ?? {})['name'] ?? '');
dirty = false;
});
async function handleReorder(ids: (string | number)[]) {
const formData = new FormData();
formData.set('ids', JSON.stringify(ids));
await fetch('?/reorder', { method: 'POST', body: formData });
await invalidateAll();
}
function openCreate() {
editingRecord = null;
formError = '';
@@ -53,16 +66,22 @@
formError = '';
fieldErrors = {};
}
function markDirty() {
if (editingRecord?.id) dirty = true;
}
</script>
<div class="p-6">
<h1 class="mb-4 text-xl font-bold text-gray-900 dark:text-gray-100">{config.displayName}</h1>
<div>
<h1>{config.displayName}</h1>
<AdminTable
records={data.records}
listFields={config.listFields}
searchFields={config.searchFields}
fieldTypes={config.fieldTypes}
singular={config.displayNameSingular}
draggable={true}
onReorder={handleReorder}
onRowClick={openEdit}
onNew={openCreate}
/>
@@ -77,14 +96,33 @@
{canDelete}
recordId={editingRecord?.id as string | undefined}
>
<LevelForm
record={editingRecord ?? {}}
{fieldErrors}
{formError}
submitLabel={editingRecord ? 'Update' : 'Create'}
/>
<form action="?/upsert" method="POST" use:enhance oninput={markDirty}>
{#if editingRecord?.id}
<input type="hidden" name="id" value={String(editingRecord.id)} />
{/if}
{#if formError}
<div>{formError}</div>
{/if}
<div>
<div>
<label for="field-name">Name</label>
<input type="text" id="field-name" name="name" bind:value={name} />
{#if fieldErrors.name}
<p>{fieldErrors.name}</p>
{/if}
</div>
<div>
<button type="submit" disabled={!!editingRecord?.id && !dirty}>
{editingRecord ? 'Save' : 'Create'}
</button>
</div>
</div>
</form>
</AdminModal>
{#if toastMessage}
<Toast message={toastMessage} type={toastType} />
<Toast message={toastMessage} />
{/if}
@@ -27,11 +27,16 @@ export const load: PageServerLoad = async ({ locals }) => {
}
const categories = await prisma.proclamationCategory.findMany({
select: { id: true, name: true, parentId: true },
select: { id: true, name: true },
orderBy: { name: 'asc' }
});
return { ...base, categories };
const groups = await prisma.proclamationGroup.findMany({
select: { id: true, name: true },
orderBy: { sortOrder: 'asc' }
});
return { ...base, categories, groups };
};
export const actions = {
@@ -54,12 +59,8 @@ export const actions = {
}
const rawId = formData.get('id')?.toString();
const { parentCategoryId, ...rest } = form.data;
const data: Record<string, unknown> = {
...rest,
categoryId: rest.categoryId ?? parentCategoryId
};
const data: Record<string, unknown> = { ...form.data };
try {
if (rawId) {
@@ -1,11 +1,15 @@
<script lang="ts">
import type { PageProps } from './$types';
import type { ActionResult } from '$lib/svelte-admin/types';
import { enhance } from '$app/forms';
import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
import Highlight from '@tiptap/extension-highlight';
import { TextStyle } from '@tiptap/extension-text-style';
import { Color } from '@tiptap/extension-color';
import AdminTable from '$lib/components/admin/AdminTable.svelte';
import AdminModal from '$lib/components/admin/AdminModal.svelte';
import Toast from '$lib/components/admin/Toast.svelte';
import { getModelConfig } from '$lib/svelte-admin/registry';
import ProclamationForm from './ProclamationForm.svelte';
let { data, form }: PageProps = $props();
@@ -17,16 +21,25 @@
let modalOpen = $state(false);
let editingRecord = $state<Record<string, unknown> | null>(null);
let toastMessage = $state('');
let toastType = $state<'success' | 'error'>('success');
let fieldErrors = $state<Record<string, string>>({});
let formError = $state('');
let dirty = $state(false);
let name = $state('');
let solfeggioFrequencyId = $state('');
let categoryId = $state('');
let groupId = $state('');
// Inline TipTap editor
let element = $state<HTMLElement>();
let editor: Editor | null = null;
let content = $state('');
$effect(() => {
if (form && 'success' in (form as any)) {
const result = form as any;
if (result.success) {
toastMessage = 'Saved successfully';
toastType = 'success';
closeModal();
} else {
formError = result.error ?? '';
@@ -35,6 +48,80 @@
}
});
$effect(() => {
const record = editingRecord ?? {};
name = String(record['name'] ?? '');
solfeggioFrequencyId = String(record['solfeggioFrequencyId'] ?? '');
categoryId = String(record['categoryId'] ?? '');
groupId = String(record['groupId'] ?? '');
content = typeof record['content'] === 'string' ? (record['content'] as string) : '';
dirty = false;
});
function markDirty() {
if (editingRecord?.id) dirty = true;
}
function handleCategoryChange(e: Event) {
categoryId = (e.target as HTMLSelectElement).value;
}
function handleGroupChange(e: Event) {
groupId = (e.target as HTMLSelectElement).value;
}
let contentKey = $derived(editingRecord?.id ?? 'new');
$effect(() => {
if (!isSuper || !modalOpen || !element) return;
const initialContent =
typeof (editingRecord ?? {})['content'] === 'string'
? ((editingRecord ?? {})['content'] as string)
: '';
editor = new Editor({
element,
extensions: [
StarterKit.configure({
bulletList: false,
orderedList: false
}),
TextStyle,
Color,
Highlight.configure({ multicolor: true })
],
content: initialContent,
onUpdate: ({ editor }) => {
content = editor.getHTML();
}
});
const instance = editor;
instance.setEditable(true);
return () => {
instance.destroy();
};
});
function toggleBold() {
editor?.chain().focus().toggleBold().run();
}
function toggleItalic() {
editor?.chain().focus().toggleItalic().run();
}
function toggleHighlight() {
editor?.chain().focus().toggleHighlight().run();
}
function setColor() {
const current = editor?.getAttributes('textStyle').color ?? '#000000';
const colors = ['#000000', '#FF0000', '#0000FF', '#008000', '#FFA500', '#800080', '#808080'];
const idx = colors.indexOf(current);
const next = colors[(idx + 1) % colors.length];
editor?.chain().focus().setColor(next).run();
}
function openCreate() {
editingRecord = null;
formError = '';
@@ -57,8 +144,8 @@
}
</script>
<div class="p-6">
<h1 class="mb-4 text-xl font-bold text-gray-900 dark:text-gray-100">{config.displayName}</h1>
<div>
<h1>{config.displayName}</h1>
<AdminTable
records={data.records}
listFields={config.listFields}
@@ -79,17 +166,109 @@
{canDelete}
recordId={editingRecord?.id as string | undefined}
>
<ProclamationForm
record={editingRecord ?? {}}
relationOptionsData={data.relationOptionsData}
categories={data.categories}
{fieldErrors}
{formError}
{isSuper}
submitLabel={editingRecord ? 'Update' : 'Create'}
/>
<form action="?/upsert" method="POST" use:enhance oninput={markDirty}>
{#if editingRecord?.id}
<input type="hidden" name="id" value={String(editingRecord.id)} />
{/if}
{#if formError}
<div>{formError}</div>
{/if}
<div>
<div>
<label for="field-name">Name</label>
<input type="text" id="field-name" name="name" bind:value={name} />
{#if fieldErrors.name}
<p>{fieldErrors.name}</p>
{/if}
</div>
<div>
<label for="field-solfeggioFrequencyId">Frequency</label>
<select
id="field-solfeggioFrequencyId"
name="solfeggioFrequencyId"
onchange={(e) => (solfeggioFrequencyId = (e.target as HTMLSelectElement).value)}
>
<option value="">-- Select Frequency --</option>
{#each data.relationOptionsData.solfeggioFrequencyId ?? [] as opt (String(opt.value))}
<option value={opt.value} selected={String(opt.value) === solfeggioFrequencyId}
>{opt.label}</option
>
{/each}
</select>
{#if fieldErrors.solfeggioFrequencyId}
<p>{fieldErrors.solfeggioFrequencyId}</p>
{/if}
</div>
<div>
<label for="field-categoryId">Category</label>
<select id="field-categoryId" name="categoryId" onchange={handleCategoryChange}>
<option value="">-- Select Category --</option>
{#each data.categories as cat (String(cat.id))}
<option value={cat.id} selected={String(cat.id) === categoryId}>{cat.name}</option>
{/each}
</select>
{#if fieldErrors.categoryId}
<p>{fieldErrors.categoryId}</p>
{/if}
</div>
<div>
<label for="field-groupId">Group</label>
<select id="field-groupId" name="groupId" onchange={handleGroupChange}>
<option value="">-- Select Group --</option>
{#each data.groups as group (String(group.id))}
<option value={group.id} selected={String(group.id) === groupId}>{group.name}</option>
{/each}
</select>
{#if fieldErrors.groupId}
<p>{fieldErrors.groupId}</p>
{/if}
</div>
{#if isSuper}
<div>
<label for="field-content">Content</label>
<div id="field-content">
{#key contentKey}
<div>
<div>
<button type="button" onclick={toggleBold} title="Bold">B</button>
<button type="button" onclick={toggleItalic} title="Italic">I</button>
<button type="button" onclick={toggleHighlight} title="Highlight">
<span>H</span>
</button>
<button type="button" onclick={setColor} title="Cycle text color">A</button>
</div>
<div
bind:this={element}
data-testid="tiptap-editor"
role="textbox"
tabindex="0"
contenteditable="true"
></div>
</div>
<input type="hidden" name="content" value={content} />
{/key}
</div>
{#if fieldErrors.content}
<p>{fieldErrors.content}</p>
{/if}
</div>
{/if}
<div>
<button type="submit" disabled={!!editingRecord?.id && !dirty}>
{editingRecord ? 'Update' : 'Create'}
</button>
</div>
</div>
</form>
</AdminModal>
{#if toastMessage}
<Toast message={toastMessage} type={toastType} />
<Toast message={toastMessage} />
{/if}
@@ -1,237 +0,0 @@
<script lang="ts">
import { enhance } from '$app/forms';
import TipTap from '$lib/components/tiptap/TipTap.svelte';
let {
record = {} as Record<string, unknown>,
fieldErrors = {} as Record<string, string>,
formError = '',
relationOptionsData = {} as Record<string, { label: string; value: string | number }[]>,
categories = [] as { id: number; name: string; parentId: number | null }[],
isSuper = false,
action = '?/upsert',
submitLabel = 'Save'
}: {
record?: Record<string, unknown>;
fieldErrors?: Record<string, string>;
formError?: string;
relationOptionsData?: Record<string, { label: string; value: string | number }[]>;
categories?: { id: number; name: string; parentId: number | null }[];
isSuper?: boolean;
action?: string;
submitLabel?: string;
} = $props();
let dirty = $state(false);
$effect(() => {
dirty = false;
});
function markDirty() {
if (record.id) dirty = true;
}
let name = $state('');
let solfeggioFrequencyId = $state('');
let subscriptionLevelId = $state('');
let parentCategoryId = $state('');
let categoryId = $state('');
$effect(() => {
name = String(record.name ?? '');
solfeggioFrequencyId = String(record.solfeggioFrequencyId ?? '');
subscriptionLevelId = String(record.subscriptionLevelId ?? '');
parentCategoryId = String(
record.parentCategoryId ??
(record.category as Record<string, unknown> | undefined)?.parentId ??
''
);
categoryId = String(record.categoryId ?? '');
});
let parentOptions = $derived(categories.filter((c) => c.parentId === null));
let childOptions = $derived(
parentCategoryId ? categories.filter((c) => c.parentId === Number(parentCategoryId)) : []
);
function handleParentChange(e: Event) {
const target = e.target as HTMLSelectElement;
parentCategoryId = target.value;
categoryId = '';
}
let contentKey = $derived(record.id ?? 'new');
</script>
<form {action} method="POST" use:enhance oninput={markDirty}>
{#if record?.id}
<input type="hidden" name="id" value={String(record.id)} />
{/if}
{#if formError}
<div class="mb-4 border border-red-300 bg-red-50 px-4 py-2 text-sm text-red-700">
{formError}
</div>
{/if}
<div class="flex flex-col gap-4">
<div>
<label
for="field-name"
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Name
</label>
<input
type="text"
id="field-name"
name="name"
bind:value={name}
class="w-full border px-3 py-1.5 text-sm {fieldErrors.name
? 'border-red-400'
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
/>
{#if fieldErrors.name}
<p class="mt-1 text-xs text-red-600">{fieldErrors.name}</p>
{/if}
</div>
<div>
<label
for="field-solfeggioFrequencyId"
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Frequency
</label>
<select
id="field-solfeggioFrequencyId"
name="solfeggioFrequencyId"
onchange={(e) => (solfeggioFrequencyId = (e.target as HTMLSelectElement).value)}
class="w-full border px-3 py-1.5 text-sm {fieldErrors.solfeggioFrequencyId
? 'border-red-400'
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
>
<option value="">-- Select Frequency --</option>
{#each relationOptionsData.solfeggioFrequencyId ?? [] as opt (String(opt.value))}
<option value={opt.value} selected={String(opt.value) === solfeggioFrequencyId}
>{opt.label}</option
>
{/each}
</select>
{#if fieldErrors.solfeggioFrequencyId}
<p class="mt-1 text-xs text-red-600">{fieldErrors.solfeggioFrequencyId}</p>
{/if}
</div>
<div>
<label
for="field-parentCategoryId"
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Category
</label>
<select
id="field-parentCategoryId"
name="parentCategoryId"
onchange={handleParentChange}
class="w-full border px-3 py-1.5 text-sm {fieldErrors.parentCategoryId
? 'border-red-400'
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
>
<option value="">-- Select Category --</option>
{#each parentOptions as cat (String(cat.id))}
<option value={cat.id} selected={String(cat.id) === parentCategoryId}>{cat.name}</option>
{/each}
</select>
{#if fieldErrors.parentCategoryId}
<p class="mt-1 text-xs text-red-600">{fieldErrors.parentCategoryId}</p>
{/if}
</div>
<div>
<label
for="field-categoryId"
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Sub-Category
</label>
<select
id="field-categoryId"
name="categoryId"
onchange={(e) => (categoryId = (e.target as HTMLSelectElement).value)}
class="w-full border px-3 py-1.5 text-sm {fieldErrors.categoryId
? 'border-red-400'
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
>
<option value="">-- Select Sub-Category --</option>
{#each childOptions as cat (String(cat.id))}
<option value={cat.id} selected={String(cat.id) === categoryId}>{cat.name}</option>
{/each}
</select>
{#if fieldErrors.categoryId}
<p class="mt-1 text-xs text-red-600">{fieldErrors.categoryId}</p>
{/if}
</div>
<div>
<label
for="field-subscriptionLevelId"
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Subscription Level
</label>
<select
id="field-subscriptionLevelId"
name="subscriptionLevelId"
onchange={(e) => (subscriptionLevelId = (e.target as HTMLSelectElement).value)}
class="w-full border px-3 py-1.5 text-sm {fieldErrors.subscriptionLevelId
? 'border-red-400'
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
>
<option value="">-- Select Level --</option>
{#each relationOptionsData.subscriptionLevelId ?? [] as opt (String(opt.value))}
<option value={opt.value} selected={String(opt.value) === subscriptionLevelId}
>{opt.label}</option
>
{/each}
</select>
{#if fieldErrors.subscriptionLevelId}
<p class="mt-1 text-xs text-red-600">{fieldErrors.subscriptionLevelId}</p>
{/if}
</div>
{#if isSuper}
<div>
<label
for="field-content"
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Content
</label>
<div id="field-content">
{#key record.id}
<TipTap
initialContent={typeof record.content === 'string' ? record.content : ''}
name="content"
/>
{/key}
</div>
{#if fieldErrors.content}
<p class="mt-1 text-xs text-red-600">{fieldErrors.content}</p>
{/if}
</div>
{/if}
<div class="flex justify-end gap-2 pt-2">
<button
type="submit"
disabled={!!record?.id && !dirty}
class="border border-gray-300 bg-gray-100 px-4 py-1.5 text-sm font-medium hover:bg-gray-200 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
{submitLabel}
</button>
</div>
</div>
</form>
@@ -1,152 +0,0 @@
import { page } from 'vitest/browser';
import { describe, it, expect, vi } from 'vitest';
import { render } from 'vitest-browser-svelte';
vi.mock('$app/forms', () => ({
enhance: () => () => {}
}));
vi.mock('$lib/components/tiptap/TipTap.svelte', () => ({
default: ($anchor: Element) => {
const div = document.createElement('div');
div.setAttribute('data-testid', 'tiptap-mock');
$anchor.replaceWith(div);
}
}));
import ProclamationForm from './ProclamationForm.svelte';
const relationOptionsData = {
solfeggioFrequencyId: [
{ label: 'UT - 396', value: 1 },
{ label: 'RE - 417', value: 2 }
],
subscriptionLevelId: [
{ label: 'Free', value: 1 },
{ label: 'Premium', value: 2 }
]
};
const categories = [
{ id: 1, name: 'Healing', parentId: null },
{ id: 2, name: 'Manifestation', parentId: null },
{ id: 3, name: 'Physical Healing', parentId: 1 },
{ id: 4, name: 'Emotional Healing', parentId: 1 }
];
describe('ProclamationForm', () => {
it('renders all basic fields in create mode', async () => {
render(ProclamationForm, {
relationOptionsData,
categories,
isSuper: true
});
await expect.element(page.getByRole('textbox', { name: 'Name' })).toBeInTheDocument();
await expect.element(page.getByRole('combobox', { name: /^Frequency$/ })).toBeInTheDocument();
await expect.element(page.getByRole('combobox', { name: /^Category$/ })).toBeInTheDocument();
await expect
.element(page.getByRole('combobox', { name: /^Sub-Category$/ }))
.toBeInTheDocument();
await expect
.element(page.getByRole('combobox', { name: /^Subscription Level$/ }))
.toBeInTheDocument();
await expect.element(page.getByRole('button', { name: 'Save' })).toBeInTheDocument();
});
it('pre-fills name input when editing an existing record', async () => {
render(ProclamationForm, {
record: {
id: 'proc-1',
name: 'Test Proclamation',
solfeggioFrequencyId: 2,
subscriptionLevelId: 2,
categoryId: 3,
parentCategoryId: 1
},
relationOptionsData,
categories,
isSuper: true,
submitLabel: 'Update'
});
await expect
.element(page.getByRole('textbox', { name: 'Name' }))
.toHaveValue('Test Proclamation');
});
it('pre-fills select values when editing an existing record', async () => {
render(ProclamationForm, {
record: {
id: 'proc-1',
name: 'Test Proclamation',
solfeggioFrequencyId: 2,
subscriptionLevelId: 2,
categoryId: 3,
parentCategoryId: 1
},
relationOptionsData,
categories,
isSuper: true
});
const freqEl = (await page
.getByRole('combobox', { name: /^Frequency$/ })
.element()) as HTMLSelectElement;
const levelEl = (await page
.getByRole('combobox', { name: /^Subscription Level$/ })
.element()) as HTMLSelectElement;
expect(freqEl.value).toBe('2');
expect(levelEl.value).toBe('2');
});
it('shows content field when isSuper is true', async () => {
render(ProclamationForm, {
record: { id: 'proc-1', name: 'Test', content: '<p>Secret</p>' },
relationOptionsData,
categories,
isSuper: true
});
await expect.element(page.getByText('Content')).toBeInTheDocument();
await expect.element(page.getByTestId('tiptap-mock')).toBeInTheDocument();
});
it('hides content field when isSuper is false', async () => {
render(ProclamationForm, {
record: { id: 'proc-1', name: 'Test', content: '<p>Secret</p>' },
relationOptionsData,
categories,
isSuper: false
});
await expect.element(page.getByText('Content')).not.toBeInTheDocument();
});
it('disables submit button when editing and not dirty', async () => {
render(ProclamationForm, {
record: { id: 'proc-1', name: 'Test' },
relationOptionsData,
categories,
isSuper: true,
submitLabel: 'Update'
});
await expect.element(page.getByRole('button', { name: 'Update' })).toBeDisabled();
});
it('enables submit button when editing and user modifies a field', async () => {
render(ProclamationForm, {
record: { id: 'proc-1', name: 'Test' },
relationOptionsData,
categories,
isSuper: true,
submitLabel: 'Update'
});
await page.getByRole('textbox', { name: 'Name' }).fill('New Name');
await expect.element(page.getByRole('button', { name: 'Update' })).toBeEnabled();
});
});
@@ -13,6 +13,9 @@ vi.mock('$lib/db', () => ({
},
proclamationCategory: {
findMany: vi.fn()
},
proclamationGroup: {
findMany: vi.fn()
}
}
}));
@@ -31,6 +34,7 @@ const mockLoadRecords = loadRecords as ReturnType<typeof vi.fn>;
const mockDeleteRecord = deleteRecord as ReturnType<typeof vi.fn>;
const mockSuperFormErrorsToFieldErrors = superFormErrorsToFieldErrors as ReturnType<typeof vi.fn>;
const mockCatFindMany = prisma.proclamationCategory.findMany as ReturnType<typeof vi.fn>;
const mockGroupFindMany = prisma.proclamationGroup.findMany as ReturnType<typeof vi.fn>;
const mockFindUnique = prisma.proclamation.findUnique as ReturnType<typeof vi.fn>;
const mockCreate = prisma.proclamation.create as ReturnType<typeof vi.fn>;
const mockUpdate = prisma.proclamation.update as ReturnType<typeof vi.fn>;
@@ -46,11 +50,13 @@ describe('proclamations admin page server', () => {
{ id: '2', name: 'B', content: '<p>secret2</p>' }
];
const relationOptionsData = {};
const categories = [{ id: 1, name: 'Healing', parentId: null }];
const categories = [{ id: 1, name: 'Healing' }];
const groups = [{ id: 1, name: 'General' }];
it('strips content for non-Super users', async () => {
mockLoadRecords.mockResolvedValue({ records: recordsWithContent, relationOptionsData });
mockCatFindMany.mockResolvedValue(categories);
mockGroupFindMany.mockResolvedValue(groups);
const locals = createLocals(mockUser({ roleId: 4 }) as any);
const result = await load({ locals } as any);
@@ -61,13 +67,15 @@ describe('proclamations admin page server', () => {
{ id: '2', name: 'B' }
],
relationOptionsData,
categories
categories,
groups
});
});
it('keeps content for Super users', async () => {
mockLoadRecords.mockResolvedValue({ records: recordsWithContent, relationOptionsData });
mockCatFindMany.mockResolvedValue(categories);
mockGroupFindMany.mockResolvedValue(groups);
const locals = createLocals(mockUser({ roleId: 1 }) as any);
const result = await load({ locals } as any);
@@ -75,7 +83,8 @@ describe('proclamations admin page server', () => {
expect(result).toEqual({
records: recordsWithContent,
relationOptionsData,
categories
categories,
groups
});
});
});
@@ -0,0 +1,174 @@
import { page } from 'vitest/browser';
import { describe, it, expect, vi } from 'vitest';
import { render } from 'vitest-browser-svelte';
import type { PageProps } from './$types';
vi.mock('$app/forms', () => ({
enhance: () => () => {}
}));
import Page from './+page.svelte';
const relationOptionsData = {
solfeggioFrequencyId: [
{ label: 'UT - 396', value: 1 },
{ label: 'RE - 417', value: 2 }
]
};
const categories = [
{ id: 1, name: 'Healing' },
{ id: 2, name: 'Manifestation' }
];
const groups = [
{ id: 1, name: 'General' },
{ id: 2, name: 'Specific' }
];
const superAdmin = {
id: 'admin-1',
name: 'Super Admin',
roleId: 1,
roleName: 'SUPER'
};
function makeData(overrides: Partial<PageProps['data']> = {}): PageProps['data'] {
return {
records: [],
relationOptionsData,
categories,
groups,
adminUser: superAdmin,
userRoleId: superAdmin.roleId,
...overrides
};
}
function mountPage(dataOverrides: Partial<PageProps['data']> = {}) {
return render(Page, { params: {}, data: makeData(dataOverrides), form: null });
}
const editRecord = {
id: 'proc-1',
name: 'Test Proclamation',
content: '<p>Secret</p>',
solfeggioFrequencyId: 2,
categoryId: 1,
groupId: 2,
category: { name: 'Healing' },
group: { name: 'Specific' },
solfeggioFrequency: { name: 'RE - 417' }
};
async function openCreateModal() {
await page.getByRole('button', { name: 'New Proclamation +' }).click();
}
async function openEditModal() {
await page.getByRole('cell', { name: 'Test Proclamation' }).click();
}
describe('proclamations admin page (super user)', () => {
it('renders all basic fields in create mode', async () => {
mountPage();
await openCreateModal();
await expect.element(page.getByRole('textbox', { name: 'Name' })).toBeInTheDocument();
await expect.element(page.getByRole('combobox', { name: /^Frequency$/ })).toBeInTheDocument();
await expect.element(page.getByRole('combobox', { name: /^Category$/ })).toBeInTheDocument();
await expect.element(page.getByRole('combobox', { name: /^Group$/ })).toBeInTheDocument();
await expect.element(page.getByRole('button', { name: 'Create' })).toBeInTheDocument();
});
it('pre-fills name input when editing an existing record', async () => {
mountPage({ records: [editRecord] });
await openEditModal();
await expect
.element(page.getByRole('textbox', { name: 'Name' }))
.toHaveValue('Test Proclamation');
});
it('pre-fills select values when editing an existing record', async () => {
mountPage({ records: [editRecord] });
await openEditModal();
const freqEl = (await page
.getByRole('combobox', { name: /^Frequency$/ })
.element()) as HTMLSelectElement;
const categoryEl = (await page
.getByRole('combobox', { name: /^Category$/ })
.element()) as HTMLSelectElement;
const groupEl = (await page
.getByRole('combobox', { name: /^Group$/ })
.element()) as HTMLSelectElement;
expect(freqEl.value).toBe('2');
expect(categoryEl.value).toBe('1');
expect(groupEl.value).toBe('2');
});
it('keeps group selection independent of category selection (movable filter)', async () => {
mountPage({ records: [editRecord] });
await openEditModal();
const categoryEl = (await page
.getByRole('combobox', { name: /^Category$/ })
.element()) as HTMLSelectElement;
const groupEl = (await page
.getByRole('combobox', { name: /^Group$/ })
.element()) as HTMLSelectElement;
categoryEl.value = '2';
categoryEl.dispatchEvent(new Event('change', { bubbles: true }));
expect(categoryEl.value).toBe('2');
expect(groupEl.value).toBe('2');
});
it('shows content editor when isSuper is true', async () => {
mountPage({ records: [editRecord] });
await openEditModal();
await expect.element(page.getByText('Content')).toBeInTheDocument();
await expect.element(page.getByTestId('tiptap-editor')).toBeInTheDocument();
});
it('disables submit button when editing and not dirty', async () => {
mountPage({ records: [editRecord] });
await openEditModal();
await expect.element(page.getByRole('button', { name: 'Update' })).toBeDisabled();
});
it('enables submit button when editing and user modifies a field', async () => {
mountPage({ records: [editRecord] });
await openEditModal();
await page.getByRole('textbox', { name: 'Name' }).fill('New Name');
await expect.element(page.getByRole('button', { name: 'Update' })).toBeEnabled();
});
});
describe('proclamations admin page (non-super user)', () => {
it('hides create/edit entry points and content field from non-super users', async () => {
mountPage({
records: [editRecord],
adminUser: { ...superAdmin, id: 'admin-2', roleId: 2, roleName: 'ADMIN' }
});
await expect
.element(page.getByRole('button', { name: 'New Proclamation +' }))
.not.toBeInTheDocument();
await expect.element(page.getByText('Content')).not.toBeInTheDocument();
});
});
@@ -15,7 +15,6 @@
let modalOpen = $state(false);
let editingRecord = $state<Record<string, unknown> | null>(null);
let toastMessage = $state('');
let toastType = $state<'success' | 'error'>('success');
let fieldErrors = $state<Record<string, string>>({});
let formError = $state('');
let addressFormOpen = $state(false);
@@ -44,7 +43,6 @@
} else if ((form as any).userError) {
userError = (form as any).userError;
} else if (form.success) {
toastType = 'success';
if ((form as any).address) {
toastMessage = 'Address saved';
const addr = (form as any).address as Record<string, unknown>;
@@ -112,8 +110,8 @@
}
</script>
<div class="p-6">
<h1 class="mb-4 text-xl font-bold text-gray-900 dark:text-gray-100">{config.displayName}</h1>
<div>
<h1>{config.displayName}</h1>
<AdminTable
records={data.records}
listFields={config.listFields}
@@ -139,100 +137,74 @@
<input type="hidden" name="id" value={String(editingRecord.id)} />
{/if}
{#if formError}
<div class="mb-4 border border-red-300 bg-red-50 px-4 py-2 text-sm text-red-700">
<div>
{formError}
</div>
{/if}
<div class="flex flex-col gap-4">
<div>
<div>
<label
for="field-userId"
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
User
</label>
<select
id="field-userId"
name="userId"
value={String(editingRecord?.userId ?? '')}
class="w-full border px-3 py-1.5 text-sm {fieldErrors.userId
? 'border-red-400'
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
>
<label for="field-userId"> User </label>
<select id="field-userId" name="userId" value={String(editingRecord?.userId ?? '')}>
<option value="">-- Select User --</option>
{#each data.relationOptionsData.userId ?? [] as opt (String(opt.value))}
<option value={opt.value}>{opt.label}</option>
{/each}
</select>
{#if fieldErrors.userId}
<p class="mt-1 text-xs text-red-600">{fieldErrors.userId}</p>
<p>{fieldErrors.userId}</p>
{/if}
</div>
<div>
<label
for="field-firstName"
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
First Name
</label>
<label for="field-email"> Email (for unregistered users) </label>
<input
type="text"
id="field-email"
name="email"
value={String(editingRecord?.email ?? '')}
placeholder="user@example.com"
/>
{#if fieldErrors.email}
<p>{fieldErrors.email}</p>
{/if}
</div>
<div>
<label for="field-firstName"> First Name </label>
<input
type="text"
id="field-firstName"
name="firstName"
value={String(editingRecord?.firstName ?? '')}
class="w-full border px-3 py-1.5 text-sm {fieldErrors.firstName
? 'border-red-400'
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
/>
{#if fieldErrors.firstName}
<p class="mt-1 text-xs text-red-600">{fieldErrors.firstName}</p>
<p>{fieldErrors.firstName}</p>
{/if}
</div>
<div>
<label
for="field-lastName"
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Last Name
</label>
<label for="field-lastName"> Last Name </label>
<input
type="text"
id="field-lastName"
name="lastName"
value={String(editingRecord?.lastName ?? '')}
class="w-full border px-3 py-1.5 text-sm {fieldErrors.lastName
? 'border-red-400'
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
/>
{#if fieldErrors.lastName}
<p class="mt-1 text-xs text-red-600">{fieldErrors.lastName}</p>
<p>{fieldErrors.lastName}</p>
{/if}
</div>
<div>
<label
for="field-phoneNumber"
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Phone Number
</label>
<label for="field-phoneNumber"> Phone Number </label>
<input
type="text"
id="field-phoneNumber"
name="phoneNumber"
value={String(editingRecord?.phoneNumber ?? '')}
class="w-full border px-3 py-1.5 text-sm {fieldErrors.phoneNumber
? 'border-red-400'
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
/>
{#if fieldErrors.phoneNumber}
<p class="mt-1 text-xs text-red-600">{fieldErrors.phoneNumber}</p>
<p>{fieldErrors.phoneNumber}</p>
{/if}
</div>
<div class="flex justify-end gap-2 pt-2">
<button
type="submit"
class="border border-gray-300 bg-gray-100 px-4 py-1.5 text-sm font-medium hover:bg-gray-200 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
<div>
<button type="submit">
{editingRecord ? 'Update' : 'Create'}
</button>
</div>
@@ -241,72 +213,36 @@
{#if editingRecord}
{@const user = editingRecord.user as Record<string, unknown> | undefined}
<div class="mt-6 border-t border-gray-200 pt-4 dark:border-gray-700">
<div class="mb-2 flex items-center justify-between">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">User</h3>
<button
type="button"
onclick={toggleUserForm}
class="text-xs font-medium text-blue-600 hover:text-blue-800 dark:text-blue-400"
>
<div>
<div>
<h3>User</h3>
<button type="button" onclick={toggleUserForm}>
{userFormOpen ? 'Cancel' : 'Edit User'}
</button>
</div>
<p class="mb-1 text-sm text-gray-600 dark:text-gray-400">
<p>
{(user?.name as string) ?? ''} ({(user?.email as string) ?? ''})
</p>
{#if userFormOpen}
<form
method="POST"
action="?/updateUser"
use:enhance
class="mt-3 rounded border border-gray-200 p-3 dark:border-gray-700"
>
<form method="POST" action="?/updateUser" use:enhance>
<input type="hidden" name="id" value={String(user?.id ?? '')} />
{#if userError}
<div class="mb-2 border border-red-300 bg-red-50 px-3 py-1.5 text-sm text-red-700">
<div>
{userError}
</div>
{/if}
<div class="flex flex-col gap-2">
<div>
<div>
<label
for="user-name"
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
Name
</label>
<input
type="text"
id="user-name"
name="name"
value={userName}
class="w-full border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
/>
<label for="user-name"> Name </label>
<input type="text" id="user-name" name="name" value={userName} />
</div>
<div>
<label
for="user-email"
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
Email
</label>
<input
type="email"
id="user-email"
name="email"
value={userEmail}
class="w-full border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
/>
<label for="user-email"> Email </label>
<input type="email" id="user-email" name="email" value={userEmail} />
</div>
<div class="flex justify-end">
<button
type="submit"
class="border border-gray-300 bg-gray-100 px-3 py-1 text-xs font-medium hover:bg-gray-200 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
Save User
</button>
<div>
<button type="submit"> Save User </button>
</div>
</div>
</form>
@@ -316,172 +252,100 @@
{#if editingRecord}
{@const addresses = (editingRecord.addresses as Record<string, unknown>[]) ?? []}
<div class="mt-6 border-t border-gray-200 pt-4 dark:border-gray-700">
<div class="mb-2 flex items-center justify-between">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Addresses</h3>
<button
type="button"
onclick={toggleAddressForm}
class="text-xs font-medium text-blue-600 hover:text-blue-800 dark:text-blue-400"
>
<div>
<div>
<h3>Addresses</h3>
<button type="button" onclick={toggleAddressForm}>
{addressFormOpen ? 'Cancel' : 'Add Address +'}
</button>
</div>
{#if addressFormOpen}
<form
method="POST"
action="?/upsertAddress"
use:enhance
class="mb-3 rounded border border-gray-200 p-3 dark:border-gray-700"
>
<form method="POST" action="?/upsertAddress" use:enhance>
<input type="hidden" name="profileId" value={String(editingRecord.id)} />
{#if addressError}
<div class="mb-2 border border-red-300 bg-red-50 px-3 py-1.5 text-sm text-red-700">
<div>
{addressError}
</div>
{/if}
<div class="grid grid-cols-2 gap-2">
<div>
<div>
<label
for="addr-street"
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
Street
</label>
<input
type="text"
id="addr-street"
name="street"
required
class="w-full border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
/>
<label for="addr-street"> Street </label>
<input type="text" id="addr-street" name="street" required />
</div>
<div>
<label
for="addr-city"
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
City
</label>
<input
type="text"
id="addr-city"
name="city"
required
class="w-full border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
/>
<label for="addr-city"> City </label>
<input type="text" id="addr-city" name="city" required />
</div>
<div>
<label
for="addr-state"
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
State
</label>
<input
type="text"
id="addr-state"
name="state"
required
class="w-full border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
/>
<label for="addr-state"> State </label>
<input type="text" id="addr-state" name="state" required />
</div>
<div>
<label
for="addr-postal"
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
Postal Code
</label>
<input
type="text"
id="addr-postal"
name="postalCode"
required
class="w-full border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
/>
<label for="addr-postal"> Postal Code </label>
<input type="text" id="addr-postal" name="postalCode" required />
</div>
<div>
<label
for="addr-country"
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
Country
</label>
<input
type="text"
id="addr-country"
name="country"
required
class="w-full border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
/>
<label for="addr-country"> Country </label>
<input type="text" id="addr-country" name="country" required />
</div>
</div>
<div class="mt-2 flex flex-wrap gap-4">
<label class="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-400">
<input type="checkbox" name="isDefaultMailing" class="h-3.5 w-3.5" />
<div>
<label>
<input type="checkbox" name="isDefaultMailing" />
Default Mailing
</label>
<label class="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-400">
<input type="checkbox" name="isDefaultBilling" class="h-3.5 w-3.5" />
<label>
<input type="checkbox" name="isDefaultBilling" />
Default Billing
</label>
<label class="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-400">
<input type="checkbox" name="isDefaultShipping" class="h-3.5 w-3.5" />
<label>
<input type="checkbox" name="isDefaultShipping" />
Default Shipping
</label>
</div>
<div class="mt-2 flex justify-end">
<button
type="submit"
class="border border-gray-300 bg-gray-100 px-3 py-1 text-xs font-medium hover:bg-gray-200 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
Save Address
</button>
<div>
<button type="submit"> Save Address </button>
</div>
</form>
{/if}
{#if addresses.length > 0}
<table class="w-full text-sm">
<table>
<thead>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase">Street</th
>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase">City</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase">State</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase">Zip</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase"
>Country</th
>
<th class="px-2 py-1 text-center text-xs font-medium text-gray-500 uppercase">M</th>
<th class="px-2 py-1 text-center text-xs font-medium text-gray-500 uppercase">B</th>
<th class="px-2 py-1 text-center text-xs font-medium text-gray-500 uppercase">S</th>
<tr>
<th>Street</th>
<th>City</th>
<th>State</th>
<th>Zip</th>
<th>Country</th>
<th>M</th>
<th>B</th>
<th>S</th>
</tr>
</thead>
<tbody>
{#each addresses as addr}
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="px-2 py-1">{addr.street as string}</td>
<td class="px-2 py-1">{addr.city as string}</td>
<td class="px-2 py-1">{addr.state as string}</td>
<td class="px-2 py-1">{addr.postalCode as string}</td>
<td class="px-2 py-1">{addr.country as string}</td>
<td class="px-2 py-1 text-center text-xs">{addr.isDefaultMailing ? '✓' : ''}</td>
<td class="px-2 py-1 text-center text-xs">{addr.isDefaultBilling ? '✓' : ''}</td>
<td class="px-2 py-1 text-center text-xs">{addr.isDefaultShipping ? '✓' : ''}</td>
<tr>
<td>{addr.street as string}</td>
<td>{addr.city as string}</td>
<td>{addr.state as string}</td>
<td>{addr.postalCode as string}</td>
<td>{addr.country as string}</td>
<td>{addr.isDefaultMailing ? '✓' : ''}</td>
<td>{addr.isDefaultBilling ? '✓' : ''}</td>
<td>{addr.isDefaultShipping ? '✓' : ''}</td>
</tr>
{/each}
</tbody>
</table>
{:else}
<p class="text-sm text-gray-500">No addresses on file.</p>
<p>No addresses on file.</p>
{/if}
</div>
{/if}
</AdminModal>
{#if toastMessage}
<Toast message={toastMessage} type={toastType} />
<Toast message={toastMessage} />
{/if}
@@ -1,11 +1,10 @@
<script lang="ts">
import type { PageProps } from './$types';
import type { ActionResult } from '$lib/svelte-admin/types';
import { enhance } from '$app/forms';
import AdminTable from '$lib/components/admin/AdminTable.svelte';
import AdminModal from '$lib/components/admin/AdminModal.svelte';
import Toast from '$lib/components/admin/Toast.svelte';
import { getModelConfig } from '$lib/svelte-admin/registry';
import FreqForm from './FreqForm.svelte';
let { data, form }: PageProps = $props();
@@ -15,16 +14,18 @@
let modalOpen = $state(false);
let editingRecord = $state<Record<string, unknown> | null>(null);
let toastMessage = $state('');
let toastType = $state<'success' | 'error'>('success');
let fieldErrors = $state<Record<string, string>>({});
let formError = $state('');
let name = $state('');
let value = $state('');
let dirty = $state(false);
$effect(() => {
if (form && 'success' in (form as any)) {
const result = form as any;
if (result.success) {
toastMessage = 'Saved successfully';
toastType = 'success';
closeModal();
} else {
formError = result.error ?? '';
@@ -33,6 +34,13 @@
}
});
$effect(() => {
const record = editingRecord ?? {};
name = String(record['name'] ?? '');
value = String(record['value'] ?? '');
dirty = false;
});
function openCreate() {
editingRecord = null;
formError = '';
@@ -53,10 +61,14 @@
formError = '';
fieldErrors = {};
}
function markDirty() {
if (editingRecord?.id) dirty = true;
}
</script>
<div class="p-6">
<h1 class="mb-4 text-xl font-bold text-gray-900 dark:text-gray-100">{config.displayName}</h1>
<div>
<h1>{config.displayName}</h1>
<AdminTable
records={data.records}
listFields={config.listFields}
@@ -77,14 +89,41 @@
{canDelete}
recordId={editingRecord?.id as string | undefined}
>
<FreqForm
record={editingRecord ?? {}}
{fieldErrors}
{formError}
submitLabel={editingRecord ? 'Update' : 'Create'}
/>
<form action="?/upsert" method="POST" use:enhance oninput={markDirty}>
{#if editingRecord?.id}
<input type="hidden" name="id" value={String(editingRecord.id)} />
{/if}
{#if formError}
<div>{formError}</div>
{/if}
<div>
<div>
<label for="field-name">Name</label>
<input type="text" id="field-name" name="name" bind:value={name} />
{#if fieldErrors.name}
<p>{fieldErrors.name}</p>
{/if}
</div>
<div>
<label for="field-value">Value</label>
<input type="number" id="field-value" name="value" bind:value step="1" />
{#if fieldErrors.value}
<p>{fieldErrors.value}</p>
{/if}
</div>
<div>
<button type="submit" disabled={!!editingRecord?.id && !dirty}>
{editingRecord ? 'Update' : 'Create'}
</button>
</div>
</div>
</form>
</AdminModal>
{#if toastMessage}
<Toast message={toastMessage} type={toastType} />
<Toast message={toastMessage} />
{/if}
@@ -1,96 +0,0 @@
<script lang="ts">
import { enhance } from '$app/forms';
let {
record = {} as Record<string, unknown>,
fieldErrors = {} as Record<string, string>,
formError = '',
action = '?/upsert',
submitLabel = 'Save'
}: {
record?: Record<string, unknown>;
fieldErrors?: Record<string, string>;
formError?: string;
action?: string;
submitLabel?: string;
} = $props();
let name = $state(String(record.name ?? ''));
let value = $state(String(record.value ?? ''));
let dirty = $state(false);
$effect(() => {
dirty = false;
});
function markDirty() {
if (record.id) dirty = true;
}
</script>
<form {action} method="POST" use:enhance oninput={markDirty}>
{#if record?.id}
<input type="hidden" name="id" value={String(record.id)} />
{/if}
{#if formError}
<div class="mb-4 border border-red-300 bg-red-50 px-4 py-2 text-sm text-red-700">
{formError}
</div>
{/if}
<div class="flex flex-col gap-4">
<div>
<label
for="field-name"
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Name
</label>
<input
type="text"
id="field-name"
name="name"
bind:value={name}
class="w-full border px-3 py-1.5 text-sm {fieldErrors.name
? 'border-red-400'
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
/>
{#if fieldErrors.name}
<p class="mt-1 text-xs text-red-600">{fieldErrors.name}</p>
{/if}
</div>
<div>
<label
for="field-value"
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Value
</label>
<input
type="number"
id="field-value"
name="value"
bind:value
step="1"
class="w-full border px-3 py-1.5 text-sm {fieldErrors.value
? 'border-red-400'
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
/>
{#if fieldErrors.value}
<p class="mt-1 text-xs text-red-600">{fieldErrors.value}</p>
{/if}
</div>
<div class="flex justify-end gap-2 pt-2">
<button
type="submit"
disabled={!!record?.id && !dirty}
class="border border-gray-300 bg-gray-100 px-4 py-1.5 text-sm font-medium hover:bg-gray-200 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
{submitLabel}
</button>
</div>
</div>
</form>
@@ -1,18 +0,0 @@
import type { PageServerLoad } from './$types';
import { getModelConfig } from '$lib/svelte-admin/registry';
import { loadRecords, upsertRecord, deleteRecord } from '$lib/svelte-admin/crud';
const config = getModelConfig('subscription-levels')!;
export const load: PageServerLoad = async ({ locals }) => {
return loadRecords(locals, config);
};
export const actions = {
upsert: async ({ request, locals }) => {
return upsertRecord(request, config, locals);
},
delete: async ({ request, locals }) => {
return deleteRecord(request, config, locals);
}
};
@@ -1,95 +0,0 @@
<script lang="ts">
import { enhance } from '$app/forms';
let {
record = {} as Record<string, unknown>,
fieldErrors = {} as Record<string, string>,
formError = '',
action = '?/upsert',
submitLabel = 'Save'
}: {
record?: Record<string, unknown>;
fieldErrors?: Record<string, string>;
formError?: string;
action?: string;
submitLabel?: string;
} = $props();
let name = $state(String(record.name ?? ''));
let description = $state(String(record.description ?? ''));
let dirty = $state(false);
$effect(() => {
dirty = false;
});
function markDirty() {
if (record.id) dirty = true;
}
</script>
<form {action} method="POST" use:enhance oninput={markDirty}>
{#if record?.id}
<input type="hidden" name="id" value={String(record.id)} />
{/if}
{#if formError}
<div class="mb-4 border border-red-300 bg-red-50 px-4 py-2 text-sm text-red-700">
{formError}
</div>
{/if}
<div class="flex flex-col gap-4">
<div>
<label
for="field-name"
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Name
</label>
<input
type="text"
id="field-name"
name="name"
bind:value={name}
class="w-full border px-3 py-1.5 text-sm {fieldErrors.name
? 'border-red-400'
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
/>
{#if fieldErrors.name}
<p class="mt-1 text-xs text-red-600">{fieldErrors.name}</p>
{/if}
</div>
<div>
<label
for="field-description"
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Description
</label>
<input
type="text"
id="field-description"
name="description"
bind:value={description}
class="w-full border px-3 py-1.5 text-sm {fieldErrors.description
? 'border-red-400'
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
/>
{#if fieldErrors.description}
<p class="mt-1 text-xs text-red-600">{fieldErrors.description}</p>
{/if}
</div>
<div class="flex justify-end gap-2 pt-2">
<button
type="submit"
disabled={!!record?.id && !dirty}
class="border border-gray-300 bg-gray-100 px-4 py-1.5 text-sm font-medium hover:bg-gray-200 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
{submitLabel}
</button>
</div>
</div>
</form>
@@ -1,11 +1,10 @@
<script lang="ts">
import type { PageProps } from './$types';
import type { ActionResult } from '$lib/svelte-admin/types';
import { enhance } from '$app/forms';
import AdminTable from '$lib/components/admin/AdminTable.svelte';
import AdminModal from '$lib/components/admin/AdminModal.svelte';
import Toast from '$lib/components/admin/Toast.svelte';
import { getModelConfig } from '$lib/svelte-admin/registry';
import UserRoleForm from './UserRoleForm.svelte';
let { data, form }: PageProps = $props();
@@ -15,16 +14,17 @@
let modalOpen = $state(false);
let editingRecord = $state<Record<string, unknown> | null>(null);
let toastMessage = $state('');
let toastType = $state<'success' | 'error'>('success');
let fieldErrors = $state<Record<string, string>>({});
let formError = $state('');
let name = $state('');
let dirty = $state(false);
$effect(() => {
if (form && 'success' in (form as any)) {
const result = form as any;
if (result.success) {
toastMessage = 'Saved successfully';
toastType = 'success';
closeModal();
} else {
formError = result.error ?? '';
@@ -33,6 +33,11 @@
}
});
$effect(() => {
name = String((editingRecord ?? {})['name'] ?? '');
dirty = false;
});
function openCreate() {
editingRecord = null;
formError = '';
@@ -53,10 +58,14 @@
formError = '';
fieldErrors = {};
}
function markDirty() {
if (editingRecord?.id) dirty = true;
}
</script>
<div class="p-6">
<h1 class="mb-4 text-xl font-bold text-gray-900 dark:text-gray-100">{config.displayName}</h1>
<div>
<h1>{config.displayName}</h1>
<AdminTable
records={data.records}
listFields={config.listFields}
@@ -77,14 +86,33 @@
{canDelete}
recordId={editingRecord?.id as string | undefined}
>
<UserRoleForm
record={editingRecord ?? {}}
{fieldErrors}
{formError}
submitLabel={editingRecord ? 'Update' : 'Create'}
/>
<form action="?/upsert" method="POST" use:enhance oninput={markDirty}>
{#if editingRecord?.id}
<input type="hidden" name="id" value={String(editingRecord.id)} />
{/if}
{#if formError}
<div>{formError}</div>
{/if}
<div>
<div>
<label for="field-name">Name</label>
<input type="text" id="field-name" name="name" bind:value={name} />
{#if fieldErrors.name}
<p>{fieldErrors.name}</p>
{/if}
</div>
<div>
<button type="submit" disabled={!!editingRecord?.id && !dirty}>
{editingRecord ? 'Update' : 'Create'}
</button>
</div>
</div>
</form>
</AdminModal>
{#if toastMessage}
<Toast message={toastMessage} type={toastType} />
<Toast message={toastMessage} />
{/if}
@@ -1,73 +0,0 @@
<script lang="ts">
import { enhance } from '$app/forms';
let {
record = {} as Record<string, unknown>,
fieldErrors = {} as Record<string, string>,
formError = '',
action = '?/upsert',
submitLabel = 'Save'
}: {
record?: Record<string, unknown>;
fieldErrors?: Record<string, string>;
formError?: string;
action?: string;
submitLabel?: string;
} = $props();
let name = $state(String(record.name ?? ''));
let dirty = $state(false);
$effect(() => {
dirty = false;
});
function markDirty() {
if (record.id) dirty = true;
}
</script>
<form {action} method="POST" use:enhance oninput={markDirty}>
{#if record?.id}
<input type="hidden" name="id" value={String(record.id)} />
{/if}
{#if formError}
<div class="mb-4 border border-red-300 bg-red-50 px-4 py-2 text-sm text-red-700">
{formError}
</div>
{/if}
<div class="flex flex-col gap-4">
<div>
<label
for="field-name"
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Name
</label>
<input
type="text"
id="field-name"
name="name"
bind:value={name}
class="w-full border px-3 py-1.5 text-sm {fieldErrors.name
? 'border-red-400'
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
/>
{#if fieldErrors.name}
<p class="mt-1 text-xs text-red-600">{fieldErrors.name}</p>
{/if}
</div>
<div class="flex justify-end gap-2 pt-2">
<button
type="submit"
disabled={!!record?.id && !dirty}
class="border border-gray-300 bg-gray-100 px-4 py-1.5 text-sm font-medium hover:bg-gray-200 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
{submitLabel}
</button>
</div>
</div>
</form>
@@ -1,6 +1,5 @@
<script lang="ts">
import type { PageProps } from './$types';
import type { ActionResult } from '$lib/svelte-admin/types';
import AdminTable from '$lib/components/admin/AdminTable.svelte';
import AdminModal from '$lib/components/admin/AdminModal.svelte';
import Toast from '$lib/components/admin/Toast.svelte';
@@ -15,7 +14,6 @@
let modalOpen = $state(false);
let editingRecord = $state<Record<string, unknown> | null>(null);
let toastMessage = $state('');
let toastType = $state<'success' | 'error'>('success');
let fieldErrors = $state<Record<string, string>>({});
let formError = $state('');
@@ -24,7 +22,6 @@
const result = form as any;
if (result.success) {
toastMessage = 'Saved successfully';
toastType = 'success';
closeModal();
} else {
formError = result.error ?? '';
@@ -55,8 +52,8 @@
}
</script>
<div class="p-6">
<h1 class="mb-4 text-xl font-bold text-gray-900 dark:text-gray-100">{config.displayName}</h1>
<div>
<h1>{config.displayName}</h1>
<AdminTable
records={data.records}
listFields={config.listFields}
@@ -87,5 +84,5 @@
</AdminModal>
{#if toastMessage}
<Toast message={toastMessage} type={toastType} />
<Toast message={toastMessage} />
{/if}
@@ -35,43 +35,27 @@
{/if}
{#if formError}
<div class="mb-4 border border-red-300 bg-red-50 px-4 py-2 text-sm text-red-700">
<div>
{formError}
</div>
{/if}
<div class="flex flex-col gap-4">
<div>
<div>
<label
for="field-roleId"
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Role
</label>
<select
id="field-roleId"
name="roleId"
bind:value={roleId}
class="w-full border px-3 py-1.5 text-sm {fieldErrors.roleId
? 'border-red-400'
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
>
<label for="field-roleId"> Role </label>
<select id="field-roleId" name="roleId" bind:value={roleId}>
<option value="">-- Select Role --</option>
{#each relationOptionsData.roleId ?? [] as opt (String(opt.value))}
<option value={opt.value}>{opt.label}</option>
{/each}
</select>
{#if fieldErrors.roleId}
<p class="mt-1 text-xs text-red-600">{fieldErrors.roleId}</p>
<p>{fieldErrors.roleId}</p>
{/if}
</div>
<div class="flex justify-end gap-2 pt-2">
<button
type="submit"
disabled={!!record?.id && !dirty}
class="border border-gray-300 bg-gray-100 px-4 py-1.5 text-sm font-medium hover:bg-gray-200 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
<div>
<button type="submit" disabled={!!record?.id && !dirty}>
{submitLabel}
</button>
</div>
+2 -2
View File
@@ -40,7 +40,7 @@ describe('(protected) layout server load', () => {
});
it('redirects to /account/setup when user has no profile', async () => {
findUnique.mockResolvedValue({ id: 'user-1', roleId: null, profile: null });
findUnique.mockResolvedValue({ id: 'user-1', roleId: null, profiles: [] });
const locals = createLocals(mockUser() as any);
const url = new URL('http://localhost/dashboard');
@@ -53,7 +53,7 @@ describe('(protected) layout server load', () => {
});
it('skips redirect when already on /account/setup', async () => {
findUnique.mockResolvedValue({ id: 'user-1', roleId: null, profile: null });
findUnique.mockResolvedValue({ id: 'user-1', roleId: null, profiles: [] });
const locals = createLocals(mockUser() as any);
const url = new URL('http://localhost/account/setup');
+3 -6
View File
@@ -5,12 +5,9 @@
<Window>
{#if $page.status === 403}
<h1 class="dark:text-gray-100">Not Authorized</h1>
<p class="dark:text-gray-300">
You don't have permission to view this page. <a
href="/"
class="dark:text-gray-400 dark:hover:text-gray-200">Go home</a
>.
<h1>Not Authorized</h1>
<p>
You don't have permission to view this page. <a href="/">Go home</a>.
</p>
{/if}
</Window>
+164 -55
View File
@@ -1,79 +1,188 @@
<script lang="ts">
import { onMount } from 'svelte';
import { authClient } from '$lib/auth-client';
import { canAccessAdmin } from '$lib/svelte-admin/roles';
import './layout.css';
import { getAllModelConfigs } from '$lib/svelte-admin/registry';
import favicon from '$lib/assets/favicon.svg';
import { resolve } from '$app/paths';
import type { Snippet } from 'svelte';
import type { LayoutData } from './$types';
import './layout.css';
let { data, children } = $props();
let { children, data }: { children: Snippet; data: LayoutData } = $props();
const session = authClient.useSession();
let isAdmin = $derived(canAccessAdmin(data.userRoleId));
let dark = $state(false);
let menuOpen = $state(false);
let adminOpen = $state(false);
onMount(() => {
dark = document.documentElement.classList.contains('dark');
});
const isAdmin = $derived(canAccessAdmin(data.userRoleId));
const adminModels = getAllModelConfigs();
function toggleTheme() {
dark = !dark;
if (dark) {
document.documentElement.classList.add('dark');
localStorage.setItem('theme', 'dark');
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('theme', 'light');
}
function closeMenu() {
menuOpen = false;
}
</script>
<svelte:head>
<link rel="icon" href={favicon} />
<title>Rhema Wave Technologies</title>
<meta
name="description"
content="Rhema Wave Technologies User Portal — manage proclamations, audio content, and solfeggio frequencies."
/>
<meta name="theme-color" content="#6b7280" />
<meta property="og:title" content="Rhema Wave Technologies" />
<meta
property="og:description"
content="Manage proclamations, audio content, and solfeggio frequencies."
/>
<title>Rhema Wave User Portal</title>
<meta name="description" content="Rhema Wave Technologies User Portal" />
<meta property="og:title" content="Rhema Wave User Portal" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Rhema Wave Technologies" />
<meta property="og:site_name" content="Rhema Wave User Portal" />
<meta name="twitter:card" content="summary" />
</svelte:head>
<nav
class="flex w-full items-center justify-between border-b border-gray-200 px-8 py-6 dark:border-gray-700 dark:bg-gray-950 dark:text-gray-100"
>
<span><a href="/" class="dark:text-gray-100">Rhema Wave Technologies</a></span>
<div class="flex items-center gap-4">
<header class="sticky top-0 z-40 border-b border-slate-200 bg-white/90 backdrop-blur">
<div class="mx-auto flex h-14 max-w-6xl items-center justify-between px-4">
<a href={resolve('/')} class="text-base font-semibold text-slate-900">Rhema Wave Portal</a>
<button
onclick={toggleTheme}
class="border border-gray-300 px-3 py-1 text-sm dark:border-gray-600 dark:text-gray-300"
type="button"
aria-label="Toggle navigation menu"
aria-expanded={menuOpen}
onclick={() => (menuOpen = !menuOpen)}
class="flex h-10 w-10 items-center justify-center rounded-lg text-slate-700 hover:bg-slate-100"
>
{dark ? 'Light' : 'Dark'}
{#if menuOpen}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg
>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><line x1="4" x2="20" y1="6" y2="6" /><line x1="4" x2="20" y1="12" y2="12" /><line
x1="4"
x2="20"
y1="18"
y2="18"
/></svg
>
{/if}
</button>
{#if $session.data}
<a href="/dashboard" class="dark:text-gray-300 dark:hover:text-gray-100">Dashboard</a>
<button
class="dark:text-gray-300 dark:hover:text-gray-100"
onclick={async () => {
await authClient.signOut();
window.location.href = '/';
}}>Log Out</button
>
{:else}
<span class="dark:text-gray-300"
><a href="/login" class="dark:text-gray-300 dark:hover:text-gray-100">Log In</a> |
<a href="/register" class="dark:text-gray-300 dark:hover:text-gray-100">Register</a></span
>
{/if}
</div>
</nav>
<div class="flex min-h-0 flex-1 px-8 dark:bg-gray-950">
</header>
{#if menuOpen}
<div
class="fixed inset-0 top-14 z-50 overflow-y-auto border-t border-slate-200 bg-white shadow-lg"
>
<nav class="mx-auto max-w-6xl px-4 py-4">
{#if $session.data?.user}
<div class="flex flex-col gap-1">
<a
href={resolve('/')}
onclick={closeMenu}
class="rounded-lg px-4 py-3 text-base font-medium text-slate-800 hover:bg-slate-100"
>
Home
</a>
<a
href={resolve('/dashboard')}
onclick={closeMenu}
class="rounded-lg px-4 py-3 text-base font-medium text-slate-800 hover:bg-slate-100"
>
Dashboard
</a>
<a
href={resolve('/account')}
onclick={closeMenu}
class="rounded-lg px-4 py-3 text-base font-medium text-slate-800 hover:bg-slate-100"
>
Account
</a>
<a
href={resolve('/license-keys')}
onclick={closeMenu}
class="rounded-lg px-4 py-3 text-base font-medium text-slate-800 hover:bg-slate-100"
>
License Keys
</a>
{#if isAdmin}
<div class="mt-2 border-t border-slate-200 pt-2">
<button
type="button"
aria-expanded={adminOpen}
onclick={() => (adminOpen = !adminOpen)}
class="flex w-full items-center justify-between rounded-lg px-4 py-3 text-base font-semibold text-indigo-700 hover:bg-indigo-50"
>
<span>Admin</span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class:rotate-180={adminOpen}
class="transition-transform"><path d="m6 9 6 6 6-6" /></svg
>
</button>
{#if adminOpen}
<div class="mt-1 flex flex-col gap-1">
{#each adminModels as model (model.slug)}
<a
href="/admin/{model.slug}"
onclick={closeMenu}
class="rounded-lg px-4 py-3 text-base text-slate-600 hover:bg-slate-100"
>
{model.displayName}
</a>
{/each}
<a
href={resolve('/admin/api-keys')}
onclick={closeMenu}
class="rounded-lg px-4 py-3 text-base text-slate-600 hover:bg-slate-100"
>
API Keys
</a>
</div>
{/if}
</div>
{/if}
</div>
{:else}
<div class="flex flex-col gap-1">
<a
href={resolve('/')}
onclick={closeMenu}
class="rounded-lg px-4 py-3 text-base font-medium text-slate-800 hover:bg-slate-100"
>
Home
</a>
<a
href={resolve('/login')}
onclick={closeMenu}
class="mt-1 rounded-lg bg-indigo-600 px-4 py-3 text-center text-base font-medium text-white hover:bg-indigo-700"
>
Login
</a>
</div>
{/if}
</nav>
</div>
{/if}
<div class="min-h-screen bg-slate-50 text-slate-900">
{@render children()}
</div>
+22 -2
View File
@@ -1,3 +1,23 @@
<div class="flex flex-1 flex-col items-center justify-center">
<h1 class="dark:text-gray-100">Hello</h1>
<script lang="ts">
import { resolve } from '$app/paths';
</script>
<div class="mx-auto w-full max-w-6xl px-4 py-12 sm:py-20">
<div class="flex flex-col items-start gap-8">
<div class="max-w-2xl">
<h1 class="text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl">
Welcome to your Rhema Wave Portal
</h1>
<p class="mt-4 text-base text-slate-600 sm:text-lg">
Manage your account and license keys all in one place.
</p>
</div>
<a
href={resolve('/login')}
class="inline-flex h-12 items-center justify-center rounded-lg bg-indigo-600 px-8 text-base font-medium text-white hover:bg-indigo-700"
>
Get Started
</a>
</div>
</div>
@@ -12,21 +12,11 @@ function generateLicenseKey(): string {
export const POST: RequestHandler = async ({ request }) => {
const body = await request.json();
const { email, name, subscription_level } = body;
const { email, name } = body;
if (!email || typeof email !== 'string') {
error(400, 'email is required');
}
if (!subscription_level || typeof subscription_level !== 'string') {
error(400, 'subscription_level is required');
}
const level = await prisma.subscriptionLevel.findFirst({
where: { name: subscription_level }
});
if (!level) {
error(400, `subscription_level "${subscription_level}" not found`);
}
let user = await prisma.user.findUnique({ where: { email } });
@@ -83,7 +73,6 @@ export const POST: RequestHandler = async ({ request }) => {
data: {
key,
active: true,
level: subscription_level,
userId: user.id
}
});
+1 -2
View File
@@ -39,8 +39,7 @@ export const POST: RequestHandler = async ({ request }) => {
return new Response(
JSON.stringify({
key: storedKey.key,
email_address: email,
subscription_level: storedKey.level
email_address: email
}),
{ headers: { 'content-type': 'application/json' } }
);
@@ -61,7 +61,7 @@ export const POST: RequestHandler = async ({ request, url }) => {
throw error(400, 'invalid JSON body');
}
const { email, name, subscription_level } = body as Record<string, unknown>;
const { email, name } = body as Record<string, unknown>;
if (typeof email !== 'string' || !email.trim()) {
error(400, 'email is required');
@@ -71,10 +71,6 @@ export const POST: RequestHandler = async ({ request, url }) => {
error(400, 'email is required');
}
if (typeof subscription_level !== 'string' || !subscription_level.trim()) {
error(400, 'subscription_level is required');
}
const emailLimit = rateLimit({
key: `onboarding:email:${cleanEmail}`,
limit: 3,
@@ -84,12 +80,6 @@ export const POST: RequestHandler = async ({ request, url }) => {
return rateLimited(emailLimit.retryAfterMs);
}
const levelName = subscription_level.trim();
const level = await prisma.subscriptionLevel.findFirst({ where: { name: levelName } });
if (!level) {
error(400, `subscription_level "${levelName}" not found`);
}
const displayName =
typeof name === 'string' && name.trim() ? name.trim().slice(0, 120) : cleanEmail.split('@')[0];
@@ -148,7 +138,6 @@ export const POST: RequestHandler = async ({ request, url }) => {
data: {
key,
active: true,
level: levelName,
userId: user.id
}
});
@@ -25,7 +25,6 @@ vi.mock('$lib/server/resend', () => ({
vi.mock('$lib/db', () => ({
prisma: {
subscriptionLevel: { findFirst: vi.fn() },
user: { findUnique: vi.fn() },
verification: { findFirst: vi.fn() },
licenseKey: { create: vi.fn() }
@@ -40,7 +39,6 @@ type POSTEvent = Parameters<typeof POST>[0];
const SECRET = 'test-onboarding-secret';
const PATH = '/api/v2/automations/create/account';
const mockLevelFindFirst = prisma.subscriptionLevel.findFirst as ReturnType<typeof vi.fn>;
const mockUserFindUnique = prisma.user.findUnique as ReturnType<typeof vi.fn>;
const mockVerification = prisma.verification.findFirst as ReturnType<typeof vi.fn>;
const mockLicenseCreate = prisma.licenseKey.create as ReturnType<typeof vi.fn>;
@@ -88,13 +86,12 @@ describe('POST /api/v2/automations/create/account', () => {
});
it('creates a license key for an existing user without a new signup', async () => {
mockLevelFindFirst.mockResolvedValue({ id: 1, name: 'Basic', description: 'General only' });
mockUserFindUnique.mockResolvedValue({ id: 'user-1', email: 'user@example.com' });
mockLicenseCreate.mockResolvedValue({ id: 'lic-1' });
sendMailMock.mockResolvedValue({});
const res = await POST({
request: signedOnboardingRequest({ email: 'user@example.com', subscription_level: 'Basic' }),
request: signedOnboardingRequest({ email: 'user@example.com' }),
url: new URL(`http://localhost${PATH}`)
} as POSTEvent);
@@ -105,12 +102,15 @@ describe('POST /api/v2/automations/create/account', () => {
user_id: 'user-1'
});
expect(authApi.signUpEmail).not.toHaveBeenCalled();
expect(mockLicenseCreate).toHaveBeenCalledTimes(1);
expect(mockLicenseCreate).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ active: true })
})
);
expect(sendMailMock).toHaveBeenCalledTimes(1);
});
it('signs up a new user and sends registration + license emails', async () => {
mockLevelFindFirst.mockResolvedValue({ id: 1, name: 'Basic', description: 'General only' });
mockUserFindUnique
.mockResolvedValueOnce(null)
.mockResolvedValueOnce({ id: 'user-2', email: 'new@example.com' });
@@ -120,7 +120,7 @@ describe('POST /api/v2/automations/create/account', () => {
authApi.signUpEmail.mockResolvedValue({});
const res = await POST({
request: signedOnboardingRequest({ email: 'new@example.com', subscription_level: 'Elite' }),
request: signedOnboardingRequest({ email: 'new@example.com' }),
url: new URL(`http://localhost${PATH}`)
} as POSTEvent);
@@ -142,7 +142,7 @@ describe('POST /api/v2/automations/create/account', () => {
const req = new Request(`http://localhost${PATH}`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ email: 'user@example.com', subscription_level: 'Basic' })
body: JSON.stringify({ email: 'user@example.com' })
});
const res = await POST({ request: req, url: new URL(`http://localhost${PATH}`) } as POSTEvent);
@@ -152,10 +152,7 @@ describe('POST /api/v2/automations/create/account', () => {
it('rejects a request signed with the wrong secret', async () => {
const res = await POST({
request: signedOnboardingRequest(
{ email: 'user@example.com', subscription_level: 'Basic' },
'wrong-secret'
),
request: signedOnboardingRequest({ email: 'user@example.com' }, 'wrong-secret'),
url: new URL(`http://localhost${PATH}`)
} as POSTEvent);
expect(res.status).toBe(401);
@@ -165,7 +162,7 @@ describe('POST /api/v2/automations/create/account', () => {
it('rejects a missing email', async () => {
await expectHttpError(
POST({
request: signedOnboardingRequest({ subscription_level: 'Basic' }),
request: signedOnboardingRequest({}),
url: new URL(`http://localhost${PATH}`)
} as POSTEvent),
400,
@@ -176,7 +173,6 @@ describe('POST /api/v2/automations/create/account', () => {
it('rejects an oversized body with 413', async () => {
const rawBody = JSON.stringify({
email: 'user@example.com',
subscription_level: 'Basic',
name: 'x'.repeat(64 * 1024 + 1)
});
expect(rawBody.length).toBeGreaterThan(64 * 1024);
@@ -194,40 +190,21 @@ describe('POST /api/v2/automations/create/account', () => {
);
});
it('rejects an unknown subscription level', async () => {
mockLevelFindFirst.mockResolvedValue(null);
await expectHttpError(
POST({
request: signedOnboardingRequest({
email: 'user@example.com',
subscription_level: 'Titanium'
}),
url: new URL(`http://localhost${PATH}`)
} as POSTEvent),
400,
'subscription_level "Titanium" not found'
);
});
it('rate limits repeated requests for the same email', async () => {
mockLevelFindFirst.mockResolvedValue({ id: 1, name: 'Basic', description: 'General only' });
mockUserFindUnique.mockResolvedValue({ id: 'user-1', email: 'user@example.com' });
mockLicenseCreate.mockResolvedValue({ id: 'lic-1' });
sendMailMock.mockResolvedValue({});
for (let i = 0; i < 3; i++) {
const res = await POST({
request: signedOnboardingRequest({
email: 'user@example.com',
subscription_level: 'Basic'
}),
request: signedOnboardingRequest({ email: 'user@example.com' }),
url: new URL(`http://localhost${PATH}`)
} as POSTEvent);
expect(res.status).toBe(200);
}
const res = await POST({
request: signedOnboardingRequest({ email: 'user@example.com', subscription_level: 'Basic' }),
request: signedOnboardingRequest({ email: 'user@example.com' }),
url: new URL(`http://localhost${PATH}`)
} as POSTEvent);
expect(res.status).toBe(429);
+11
View File
@@ -0,0 +1,11 @@
import type { RequestHandler } from './$types';
import { prisma } from '$lib/db';
import { jsonOk } from '$lib/server/v2-helpers';
import { serializeGroup } from '$lib/server/v2-serializers';
export const GET: RequestHandler = async () => {
const groups = await prisma.proclamationGroup.findMany({
orderBy: { sortOrder: 'asc' }
});
return jsonOk(groups.map(serializeGroup));
};
+47
View File
@@ -0,0 +1,47 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
vi.mock('$lib/db', () => ({
prisma: {
proclamationGroup: { findMany: vi.fn() }
}
}));
import { prisma } from '$lib/db';
import { GET } from './+server';
type GETEvent = Parameters<typeof GET>[0];
const mockFindMany = prisma.proclamationGroup.findMany as ReturnType<typeof vi.fn>;
describe('GET /api/v2/groups', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('returns groups ordered by sortOrder', async () => {
mockFindMany.mockResolvedValue([
{ id: 1, name: 'General', sortOrder: 0 },
{ id: 2, name: 'Specific', sortOrder: 1 },
{ id: 3, name: 'Precise', sortOrder: 2 }
]);
const res = await GET({} as GETEvent);
expect(res.status).toBe(200);
expect(await res.json()).toEqual([
{ id: 1, name: 'General', sortOrder: 0 },
{ id: 2, name: 'Specific', sortOrder: 1 },
{ id: 3, name: 'Precise', sortOrder: 2 }
]);
expect(mockFindMany).toHaveBeenCalledWith({ orderBy: { sortOrder: 'asc' } });
});
it('returns an empty list when no groups exist', async () => {
mockFindMany.mockResolvedValue([]);
const res = await GET({} as GETEvent);
expect(res.status).toBe(200);
expect(await res.json()).toEqual([]);
});
});
+1 -7
View File
@@ -50,16 +50,10 @@ export const POST: RequestHandler = async ({ request }) => {
throw error(400, 'key does not belong to specified user');
}
const level = await prisma.subscriptionLevel.findFirst({
where: { name: storedKey.level }
});
return jsonOk(
serializeLicense({
key: storedKey.key,
emailAddress: email,
levelName: storedKey.level,
level
emailAddress: email
})
);
};
+6 -30
View File
@@ -5,8 +5,7 @@ import { _clearRateLimits } from '$lib/server/v2-rate-limit';
vi.mock('$lib/db', () => ({
prisma: {
licenseKey: { findFirst: vi.fn() },
user: { findUnique: vi.fn() },
subscriptionLevel: { findFirst: vi.fn() }
user: { findUnique: vi.fn() }
}
}));
@@ -17,7 +16,6 @@ type POSTEvent = Parameters<typeof POST>[0];
const mockKeyFindFirst = prisma.licenseKey.findFirst as ReturnType<typeof vi.fn>;
const mockUserFindUnique = prisma.user.findUnique as ReturnType<typeof vi.fn>;
const mockLevelFindFirst = prisma.subscriptionLevel.findFirst as ReturnType<typeof vi.fn>;
function licenseRequest(body: unknown): Request {
return new Request('http://localhost/api/v2/license', {
@@ -44,7 +42,6 @@ const storedKey = {
id: 'key-1',
key: 'ABCD-EFGH-IJKL-MNOP',
active: true,
level: 'Advanced',
userId: 'user-1'
};
@@ -54,42 +51,21 @@ describe('POST /api/v2/license', () => {
_clearRateLimits();
});
it('returns the subscription_level object for a valid key', async () => {
it('returns key and email_address for a valid key', async () => {
mockKeyFindFirst.mockResolvedValue(storedKey);
mockUserFindUnique.mockResolvedValue({ id: 'user-1', email: 'user@example.com' });
mockLevelFindFirst.mockResolvedValue({
id: 2,
name: 'Advanced',
description: 'General + Specific'
});
const res = await POST({
request: licenseRequest({ license_key: storedKey.key, email_address: 'user@example.com' })
} as POSTEvent);
expect(res.status).toBe(200);
expect(await res.json()).toEqual({
const body = (await res.json()) as Record<string, unknown>;
expect(body).toEqual({
key: storedKey.key,
email_address: 'user@example.com',
subscription_level: { id: 2, name: 'Advanced', description: 'General + Specific' }
});
});
it('falls back to a structurally consistent subscription_level for an unknown level name', async () => {
mockKeyFindFirst.mockResolvedValue({ ...storedKey, level: 'SomeCustomLevel' });
mockUserFindUnique.mockResolvedValue({ id: 'user-1', email: 'user@example.com' });
mockLevelFindFirst.mockResolvedValue(null);
const res = await POST({
request: licenseRequest({ license_key: storedKey.key, email_address: 'user@example.com' })
} as POSTEvent);
expect(res.status).toBe(200);
expect(await res.json()).toEqual({
key: storedKey.key,
email_address: 'user@example.com',
subscription_level: { id: null, name: 'SomeCustomLevel', description: null }
email_address: 'user@example.com'
});
expect(Object.keys(body)).not.toContain('subscription_level');
});
it('rejects a missing license_key', async () => {
@@ -8,6 +8,7 @@ import {
type ProclamationWithFrequency
} from '$lib/server/v2-serializers';
// groupId is a scalar column; no relation include needed for serialization.
const PROCLAMATION_INCLUDE = { solfeggioFrequency: true } as const;
export const GET: RequestHandler = async ({ request }) => {
@@ -38,7 +38,8 @@ const peaceProclamation = {
name: 'Peace & Relaxation',
content: '<p>The text content</p>',
categoryId: 5,
subscriptionLevelId: 1,
groupId: 1,
solfeggioFrequencyId: 5,
solfeggioFrequency: { id: 5, name: 'Solar Plexus', value: 528 }
};
@@ -48,7 +49,7 @@ const crown936 = {
name: 'Crown 936',
content: '<p>no canonical id</p>',
categoryId: 4,
subscriptionLevelId: 1,
solfeggioFrequencyId: 1,
solfeggioFrequency: { id: 1, name: 'Crown', value: 936 }
};
@@ -88,8 +89,8 @@ describe('GET /api/v2/proclamations', () => {
name: peaceProclamation.name,
content: peaceProclamation.content,
categoryId: 5,
groupId: 1,
solfeggioFrequencyId: 5,
subscriptionLevelId: 1,
colorPalette: palettes[0].colors
}
]);
@@ -1,11 +0,0 @@
import type { RequestHandler } from './$types';
import { prisma } from '$lib/db';
import { jsonOk } from '$lib/server/v2-helpers';
import { serializeSubscriptionLevel } from '$lib/server/v2-serializers';
export const GET: RequestHandler = async () => {
const levels = await prisma.subscriptionLevel.findMany({
orderBy: { id: 'asc' }
});
return jsonOk(levels.map(serializeSubscriptionLevel));
};
+45 -37
View File
@@ -1,49 +1,57 @@
<script lang="ts">
import { resolve } from '$app/paths';
import type { PageProps } from './$types';
import Window from '$lib/components/containers/Window.svelte';
let { form }: PageProps = $props();
</script>
<div class="mx-auto mt-12 max-w-md dark:text-gray-100">
<h1 class="mb-6 text-2xl font-semibold text-gray-900 dark:text-gray-100">Forgot Password</h1>
<Window>
{#if form?.success}
<div
class="mb-4 rounded-lg border border-green-200 bg-green-50 p-4 text-green-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
If an account with that email exists, we've sent a password reset link.
<div class="rounded-lg bg-green-50 px-4 py-6 text-center">
<h1 class="text-xl font-bold text-slate-900">Check your email</h1>
<p class="mt-3 text-sm text-slate-600">
If an account with that email exists, we've sent a password reset link.
</p>
</div>
{/if}
{:else}
{#if form?.error && !form?.success}
<div class="mb-4 rounded-lg bg-red-50 px-4 py-3 text-sm text-red-700" role="alert">
{form.error}
</div>
{/if}
{#if form?.error && !form?.success}
<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 method="POST" class="flex flex-col gap-4">
<h1 class="text-2xl font-bold text-slate-900">Forgot Password</h1>
<p class="text-sm text-slate-600">
Enter your email and we'll send you a link to reset your password.
</p>
<div class="flex flex-col gap-1.5">
<label for="email" class="text-sm font-medium text-slate-700"> Email Address </label>
<input
id="email"
type="email"
name="email"
placeholder="you@example.com"
required
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
/>
</div>
<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"
>
Send Reset Link
</button>
</form>
<a
href={resolve('/login')}
class="mt-6 block text-center text-sm text-indigo-600 hover:text-indigo-700"
>
{form.error}
</div>
Back to Login
</a>
{/if}
<form method="POST" class="flex flex-col gap-4">
<input
type="email"
name="email"
placeholder="Email Address"
required
class="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"
/>
<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"
>
Send Reset Link
</button>
</form>
<a
href={resolve('/login')}
class="mt-4 block text-sm text-blue-600 hover:text-blue-800 dark:text-gray-400 dark:hover:text-gray-200"
>
Back to Login
</a>
</div>
</Window>
+5 -91
View File
@@ -2,95 +2,9 @@
@plugin '@tailwindcss/forms';
@plugin '@tailwindcss/typography';
/* Use class-based dark mode instead of prefers-color-scheme */
@custom-variant dark (&:where(.dark, .dark *));
/* ------------------------------------------------------------------ */
/* Wireframe base — remap all non-gray colors to grayscale */
/* Use var(--color-gray-N) so dark-mode inversion flows through */
/* ------------------------------------------------------------------ */
@theme {
--color-blue-50: var(--color-gray-100);
--color-blue-100: var(--color-gray-200);
--color-blue-200: var(--color-gray-300);
--color-blue-300: var(--color-gray-400);
--color-blue-400: var(--color-gray-500);
--color-blue-500: var(--color-gray-500);
--color-blue-600: var(--color-gray-600);
--color-blue-700: var(--color-gray-700);
--color-blue-800: var(--color-gray-800);
--color-green-50: var(--color-gray-100);
--color-green-100: var(--color-gray-200);
--color-green-200: var(--color-gray-300);
--color-green-300: var(--color-gray-400);
--color-green-400: var(--color-gray-500);
--color-green-500: var(--color-gray-500);
--color-green-600: var(--color-gray-600);
--color-green-700: var(--color-gray-700);
--color-green-800: var(--color-gray-800);
--color-red-50: var(--color-gray-100);
--color-red-100: var(--color-gray-200);
--color-red-200: var(--color-gray-300);
--color-red-300: var(--color-gray-400);
--color-red-400: var(--color-gray-500);
--color-red-500: var(--color-gray-500);
--color-red-600: var(--color-gray-600);
--color-red-700: var(--color-gray-700);
--color-red-800: var(--color-gray-800);
--color-purple-50: var(--color-gray-100);
--color-purple-100: var(--color-gray-200);
--color-purple-200: var(--color-gray-300);
--color-purple-300: var(--color-gray-400);
--color-purple-400: var(--color-gray-500);
--color-purple-500: var(--color-gray-500);
--color-purple-600: var(--color-gray-600);
--color-purple-700: var(--color-gray-700);
--color-purple-800: var(--color-gray-800);
--color-purple-900: var(--color-gray-900);
--color-slate-50: var(--color-gray-50);
--color-slate-100: var(--color-gray-100);
--color-slate-200: var(--color-gray-200);
--color-slate-300: var(--color-gray-300);
--color-slate-400: var(--color-gray-400);
--color-slate-500: var(--color-gray-500);
--color-slate-600: var(--color-gray-600);
--color-slate-700: var(--color-gray-700);
--color-slate-800: var(--color-gray-800);
--color-slate-900: var(--color-gray-900);
--radius: 0px;
--radius-sm: 0px;
--radius-md: 0px;
--radius-lg: 0px;
--radius-xl: 0px;
--radius-2xl: 0px;
--radius-3xl: 0px;
--shadow-sm: 0 0 #0000;
--shadow: 0 0 #0000;
--shadow-md: 0 0 #0000;
--shadow-lg: 0 0 #0000;
--shadow-xl: 0 0 #0000;
--shadow-2xl: 0 0 #0000;
}
/* ------------------------------------------------------------------ */
/* Dark mode form elements */
/* ------------------------------------------------------------------ */
.dark input,
.dark select,
.dark textarea {
background-color: #1f2937;
color: #f9fafb;
border-color: #374151;
}
/* ------------------------------------------------------------------ */
/* Force-disable transitions, animations, and remaining decoration */
/* ------------------------------------------------------------------ */
*,
::before,
::after {
transition: none !important;
animation: none !important;
@layer base {
body {
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
}

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