Compare commits

...
79 Commits
Author SHA1 Message Date
NeilClack e86e90137b Merge pull request 'Dev' (#1) from dev into main
Build and Push to Digital Ocean Container Registry / build-and-push (push) Successful in 44s
Build and Push to Digital Ocean Container Registry / migrate (push) Successful in 23s
Reviewed-on: #1
2026-09-30 04:29:28 +00:00
NeilClack 2aa6669007 Migrate actions to new runner 2026-09-29 23:28:50 -05:00
NeilClack 87934dfd42 chore(vscode): remove debugger configuration 2026-09-29 15:22:03 -05:00
NeilClack 220ff92e78 Portal: fix schedule type toggle, daily rendering, overnight spill, and mobile grid 2026-09-29 15:22:02 -05:00
NeilClack cc4aa8717e chore(vscode): fix dev-server task matcher and add debuggable-Chrome task
Disable Vite's ANSI output (NO_COLOR/FORCE_COLOR) so the background-task
problem matcher's endsPattern reliably matches, fixing the
'Error exists after running preLaunchTask' failure. Add a
'Launch Debuggable Chrome' task (:9222, persistent ~/.chrome-rw-debug
profile) and an attach compound so you can debug the frontend in a Chrome
you control and stay logged in. Update DEBUGGING.md.
2026-09-29 13:41:18 -05:00
NeilClack e9f94b72ee chore: prettier-format captured Portal fixtures 2026-09-29 13:29:55 -05:00
NeilClack 04e1f79f37 chore(vscode): separate backend and frontend debug configs
Backend configs run Vite directly (not via npm run dev) so the Node
inspector owns the process that executes SSR code, with source maps
enabled for .ts server files. Frontend configs launch/attach Chrome.
Adds a Node-inspector dev task, a Full Stack compound, and DEBUGGING.md
documenting the Safari limitation and workflows.
2026-09-29 13:29:55 -05:00
NeilClack bbf4ac12ee Portal: calendar-style weekly schedule grid, responsive on mobile
Replace the stacked per-day lists with a Google-Calendar-style WeekSchedule:
day columns side by side over a shared, sticky time gutter. Click or drag an
empty slot to create a block at that time; click a block to edit it. The
daily schedule reuses the same grid with a single 'Every day' column so both
modes look consistent. On narrow screens the row scrolls horizontally with
short day labels and the gutter pinned, keeping it usable on phones. Also
change the type switcher and copy dialog to enhanced form submissions (no
full-page reload) so they respond immediately. Removes the now-unused
DaySchedule component.
2026-09-29 13:09:48 -05:00
NeilClack bf465eca60 docs: record Phase 5 verification results 2026-09-29 12:48:26 -05:00
NeilClack 6a3ecaf734 docs: Phase 5 finalize contract, add release notes, sync fixtures 2026-09-29 12:47:33 -05:00
NeilClack e6021a6345 test: Phase 5 verification scripts and opt-in e2e integration spec
Adds scripts to seed scenario data, create Proclamations, snapshot schedules,
drive the Kiosk-facing HTTP endpoints, and an opt-in (P5_INTEGRATION=1)
Vitest spec exercising create/type/copy against a scratch database. The
integration spec is skipped by default so the normal suite needs no DB.
2026-09-29 12:46:04 -05:00
NeilClack be303dcd9c docs: finalize Phase 4 schedule copy in the contract 2026-09-29 10:50:18 -05:00
NeilClack fcb5db458b Portal: Copy to… schedule UI
Adds a Copy to… action on a system's schedule with a multi-select of the
account's other systems, labelled by System Name (fallback: key), excluding
the source. Targets that already have a schedule are flagged and require an
explicit Overwrite & copy confirmation. Shows a summary of the systems
updated after success.
2026-09-29 10:49:15 -05:00
NeilClack 242be5d241 Portal: copy action and hasSchedule flag in the schedule loader
Adds a session-authenticated copy action that scopes the source and targets
to the caller's License Keys and returns a summary of the systems updated.
The loader now reports hasSchedule per system so the Copy UI can warn before
overwriting. Adds action tests (auth, cross-account, summary, error).
2026-09-29 10:49:10 -05:00
NeilClack 68a979f1a1 Portal: transactional schedule copy service
copySchedule copies one License Key's Schedule to one or more other keys by
value in a single Prisma transaction (all targets or none), assigning fresh
Block IDs and preserving each block's dynamicAudio and Proclamation order.
Source/targets are scoped to the caller's account; self-copy and empty
target lists are rejected. The resulting schedule is run through the shared
validator before commit. Adds tests: daily/weekly copy, overwrite, multiple
targets, rollback, cross-account rejection, new IDs, changed version/ETag,
copy independence, and Kiosk-payload parity with the shared fixtures.
2026-09-29 10:49:06 -05:00
NeilClack f6c937b892 docs: document Phase 3 schedule endpoint, model, and resolved open items 2026-09-29 10:35:44 -05:00
NeilClack 9a2888af36 Portal: per-system daily/weekly schedule editor
System picker lists the account's License Keys labelled by System Name
(fallback: key). Schedule type selector (Daily|Weekly) with a confirmation
before switching a populated schedule (daily->weekly prefills all seven
days; otherwise clears). Weekly shows a per-day editor. Blocks use
TimeInput, allow adjacent/midnight-crossing ranges (labelled 'ends next
day'), block empty blocks client-side, default dynamic audio on, and show
title/category/group only. Adds gap helper text and an informational
kiosk-timezone note (browser fallback; no external/paid dependency).
2026-09-29 10:28:05 -05:00
NeilClack 3bb8e0189b Portal: per-license schedule service, shared validator, and key-only GET
Implements the shared validateSchedule validator (daily + weekly, midnight
spillover, Sunday->Monday wrap, adjacency, empty-block rejection), a
per-License-Key schedule service (get/set type, CRUD blocks, weekly
prefill with confirmation), the content-hash version/ETag serializer, the
new GET /api/v2/licenses/[licenseKey]/schedule (200/304/404, type null),
and additive evolution of the legacy GET /api/v2/schedule. Adds validator,
serializer (against docs/fixtures), endpoint ETag/304, and service tests.
2026-09-29 10:23:10 -05:00
NeilClack 138bf03ea9 Portal: move Schedule to per-License-Key with daily/weekly and Block.day
Adds Schedule.licenseKeyId (unique, nullable) and Schedule.type, makes the
legacy userId optional/non-unique, adds Block.day and flips Block
dynamicAudio default to true. The migration backfills each existing
per-user schedule onto that account's oldest License Key as a daily
schedule, preserves unmappable schedules (licenseKeyId NULL) with a
NOTICE, and is reversible. Tested up/down against a scratch schema.
2026-09-29 10:11:21 -05:00
NeilClack 25b5eeb144 docs: document System Name PUT endpoint and validation fields in OpenAPI 2026-09-29 09:59:17 -05:00
NeilClack 3c49f425d8 Portal: display System Name read-only on dashboard and admin lists
Show the System Name where license keys are listed, falling back to an
'Unnamed system' indication when unset. No rename UI or mutation path.
2026-09-29 09:51:58 -05:00
NeilClack 8412dfb69c Portal: System Name PUT endpoint and license-validation status
Add PUT /api/v2/licenses/[licenseKey]/system-name (200/400 invalid_name/
404 unknown_license/409 name_taken, idempotent) and extend POST
/api/v2/license with an optional systemName and additive systemNameStatus
(ok/unchanged/conflict/invalid). Validation always succeeds; an omitted
field never changes or clears a name and empty/whitespace is invalid.
Adds server and helper tests covering trimming, bounds, case-insensitive
uniqueness, cross-account reuse, 404, and idempotency.
2026-09-29 09:51:54 -05:00
NeilClack 1d76e8dff9 Portal: add nullable System Name to LicenseKey with per-account unique index
Adds systemName (as typed, trimmed) and systemNameLower (case-folded) to
LicenseKey plus a unique index on (userId, systemNameLower) so names are
case-insensitively unique per account when non-null. Additive and
reversible; migration tested up/down. Regenerates the Prisma client.
2026-09-29 09:51:48 -05:00
NeilClack 179df1e252 Portal: 12-hour schedule times and dynamicAudio default on
Use TimeInput for Block start/end and show the day timeline axis with
AM/PM, so no 24-hour time is displayed in the schedule UI. New Blocks now
default dynamicAudio to true in the modal and server-side when the field
is absent; existing stored Blocks are untouched.
2026-09-29 08:53:22 -05:00
NeilClack 35558b0652 Portal: add reusable 12-hour TimeInput component
TimeInput displays and accepts AM/PM times, offers the 5-minute options as
suggestions, and allows typing any minute 00-59; its value model is a 24h
HH:MM string. Adds isomorphic parseTimeInput/hmToMinutes/fiveMinuteOptions
helpers and unit + component tests for them.
2026-09-29 08:53:17 -05:00
NeilClack 6c89a89050 docs: add Phase 0 schedule API/data contract and fixtures
Add docs/schedule-api-contract.md (current state, time semantics, schedule
data model, validation rules, endpoints, sync behavior, System Name rules,
dynamic audio, backward compatibility, per-phase touchpoints, open items,
changelog) and docs/fixtures/*.json (daily, weekly, none, midnight-crossing,
adjacent, dynamic-audio, and legacy v2 payloads). Kept byte-identical with
the radiantwave repo.
2026-09-29 08:36:31 -05:00
NeilClack dca9c54d83 Merge branch 'dev'
Build and Push to Digital Ocean Container Registry / build-and-push (push) Successful in 3m23s
Build and Push to Digital Ocean Container Registry / migrate (push) Successful in 41s
2026-09-11 19:07:21 -05:00
NeilClack c19c13b192 Document dynamic audio and add a schedule response example 2026-09-11 18:45:59 -05:00
NeilClack ec826093eb Expose dynamic audio on the v2 schedule API 2026-09-11 18:41:57 -05:00
NeilClack 96a1342c10 Add Enable Dynamic Audio option to schedule blocks 2026-09-11 18:41:53 -05:00
NeilClack 468a65a688 Add dynamic audio flag to schedule blocks 2026-09-11 18:41:46 -05:00
NeilClack e934a98f00 Use a 12-hour clock with AM/PM selects in the block modal 2026-09-11 18:30:26 -05:00
NeilClack 15bb51582b Move Cancel left and rename Back to Accept in the block modal 2026-09-11 18:30:08 -05:00
NeilClack 46d7cf66b8 Render overnight blocks across midnight in the day view 2026-09-11 18:24:54 -05:00
NeilClack 177b7bb13a Flag overnight blocks in the v2 schedule API 2026-09-11 18:24:51 -05:00
NeilClack 750a9286c9 Support overnight blocks in schedule validation 2026-09-11 18:24:48 -05:00
NeilClack c177e7953d Document the v2 schedule endpoint 2026-09-11 18:16:08 -05:00
NeilClack 5f4116b8e8 Add schedule page with block editor and proclamation browser 2026-09-11 18:16:06 -05:00
NeilClack 545fb22910 Add v2 schedule API endpoint 2026-09-11 18:16:04 -05:00
NeilClack 4922e941ae Add schedule validation and service layer 2026-09-11 18:16:00 -05:00
NeilClack e6b492a335 Add schedule data model and migration 2026-09-11 18:15:57 -05:00
NeilClack 051520b941 Fix page refresh issue
Build and Push to Digital Ocean Container Registry / build-and-push (push) Successful in 2m36s
Build and Push to Digital Ocean Container Registry / migrate (push) Successful in 34s
2026-09-11 16:05:43 -05:00
NeilClack 64715c91d3 Update workflow
Build and Push to Digital Ocean Container Registry / build-and-push (push) Successful in 4m6s
Build and Push to Digital Ocean Container Registry / migrate (push) Successful in 1m50s
2026-09-11 15:36:03 -05:00
NeilClack 960673bbc6 Add a site-wide dark mode
Build and Push to Digital Ocean Container Registry / build-and-push (push) Failing after 4s
Build and Push to Digital Ocean Container Registry / migrate (push) Skipped
2026-09-11 12:49:46 -05:00
NeilClack a909db39b1 Delete "API Key" page 2026-09-11 12:44:44 -05:00
NeilClack 020289966d Remove "User Roles" admin page 2026-09-11 12:44:25 -05:00
NeilClack fa614b8c37 Fix staff(+) unable to edit or delete addresses 2026-09-11 12:43:41 -05:00
NeilClack cf8fc7b719 Fix users unable to edit or delete their own addresses 2026-09-11 12:42:35 -05:00
NeilClack 848e646880 Remove the extra page separator in the Login page 2026-09-11 12:41:49 -05:00
NeilClack 98d9b095d9 Document v2 API with OpenAPI schema 2026-09-10 16:19:53 -05:00
NeilClack 2bbee98594 Add dedicated v2 proclamation category/group endpoints and catalog specs 2026-09-10 16:19:53 -05:00
NeilClack 4d929df604 Remove stale admin overhaul and API update docs 2026-09-10 16:19:52 -05:00
NeilClack 10aad07e6d Add admin Audit Log page 2026-09-10 15:10:16 -05:00
NeilClack d16965dab6 Add AuditLog table and Prisma audit extension 2026-09-10 15:10:11 -05:00
NeilClack 7596937f30 Enforce roleId checks on all admin page actions 2026-09-10 14:59:32 -05:00
NeilClack 0cb4b2148a Expose roleId on the Better Auth session user 2026-09-10 14:59:28 -05:00
NeilClack 9de0e2e25a Fix Super admins being blocked from editing proclamations 2026-09-10 14:50:08 -05:00
NeilClack 9cd5397395 Add Resend email template HTML files 2026-09-10 14:35:54 -05:00
NeilClack f6b8cad15e Add subject to password reset email 2026-09-10 14:14:45 -05:00
NeilClack 29d53af8dd Remove unauthenticated /send email endpoint 2026-09-10 14:14:44 -05:00
NeilClack 809ced6d8a Automated signups use the registration-email set-password link 2026-09-10 14:08:56 -05:00
NeilClack 84c905c9ff Add shared registration invite helper and use it for admin-created users 2026-09-10 14:08:51 -05:00
NeilClack ae8d661364 Fix reset-password page to look up the reset token correctly 2026-09-10 13:42:36 -05:00
NeilClack 5ec3018f8b Email a set-password link when an admin creates a user 2026-09-10 13:42:33 -05:00
NeilClack 8a3c1587b4 Fix admin modals getting stuck after a successful submit 2026-09-10 13:33:33 -05:00
NeilClack e60d61e90a Fix admin modals replaying old action results after create or edit 2026-09-10 13:26:54 -05:00
NeilClack f1d80279e9 Record last validation time in v2 license endpoint 2026-09-10 13:18:22 -05:00
NeilClack 4979d955c2 Show active check and last verified date on dashboard license table 2026-09-10 13:18:18 -05:00
NeilClack 0175cab0b4 Add lastValidatedAt to license keys 2026-09-10 13:18:15 -05:00
NeilClack 1aecc51bbd Replace account and license-keys pages with profile page and dashboard 2026-09-10 13:11:41 -05:00
NeilClack f50eb34769 Add createdAt to license keys 2026-09-10 13:11:34 -05:00
NeilClack 4a1777de87 Keep legacy subscription tables as unmanaged instead of dropping them 2026-09-10 11:48:49 -05:00
NeilClack d89fc6837c Add default roleId 4 to new users 2026-09-10 11:48:20 -05:00
NeilClack 5e8851e070 Fix lint debt from admin overhaul; make check/lint/test/build green
- Scope @typescript-eslint/no-explicit-any off for *.spec.ts test doubles
- Type enhance() submit callbacks with SubmitFunction in api-keys page
- Drop as-any casts in proclamations upsert by using typed form data
- Fix useless regex escapes in profiles phone validation
- Remove unused locals param from api-keys load
- Use resolve() for admin nav/error links; store literal hrefs in adminModels
- Update README admin section and ADMIN_OVERHAUL lint caveat
2026-09-10 09:43:16 -05:00
NeilClack 91d162c746 Rebuild admin pages around DataTable/AdminModal; restructure categories/groups and v2 groups API
Remove the generic $lib/svelte-admin CRUD layer and AdminTable.svelte; each
admin route now owns its own form/modal logic. Add DataTable.svelte,
superform-errors.ts, and profile-specific modal/field components.

Flatten proclamation categories/groups and drop subscription levels; add the
v2 groups API; update v1/v2 endpoints, serializers, auth, and regenerated
Prisma client with the new migrations.

Document the overhaul plan in docs/ADMIN_OVERHAUL.md.
2026-09-10 09:38:01 -05:00
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
NeilClack 86b6720676 Move to pure git clone
Build and Push to Digital Ocean Container Registry / build-and-push (push) Successful in 1m23s
Build and Push to Digital Ocean Container Registry / migrate (push) Canceled after 0s
2026-08-17 21:32:37 -05:00
NeilClack 94c9877eab Update action URLs to Github
Build and Push to Digital Ocean Container Registry / build-and-push (push) Failing after 36s
Build and Push to Digital Ocean Container Registry / migrate (push) Skipped
2026-08-17 21:29:42 -05:00
236 changed files with 26212 additions and 11105 deletions
+4 -1
View File
@@ -5,5 +5,8 @@ GOOGLE_CLIENT_ID=6
GOOGLE_CLIENT_SECRET=
RESEND_API_KEY=
EMAIL_FROM=
RESEND_TEMPLATE_ID=
# Optional. Resend template for the admin "set your password" invite email.
# Defaults to `registration-email`. Expected variables:
# `target_email_address`, `registration_link`.
RESEND_REGISTRATION_TEMPLATE_ID=
API_V2_ONBOARDING_SECRET=
+14 -6
View File
@@ -6,11 +6,15 @@ on:
jobs:
build-and-push:
runs-on: build-server
runs-on: orion-lxc
steps:
- name: Checkout
uses: actions/checkout@v4
run: |
rm -rf ./* ./.[!.]* 2>/dev/null || true
git config --global http.extraheader "Authorization: token ${{ github.token }}"
git clone --branch "${{ gitea.ref_name }}" \
"${{ gitea.server_url }}/${{ gitea.repository }}.git" .
- name: Build image
run: |
@@ -20,7 +24,7 @@ jobs:
.
- name: Login to DigitalOcean Registry
uses: docker/login-action@v3
uses: https://github.com/docker/login-action@v3
with:
registry: registry.digitalocean.com
username: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
@@ -33,14 +37,18 @@ jobs:
migrate:
needs: build-and-push
runs-on: ubuntu-latest
runs-on: orion-lxc
steps:
- name: Checkout
uses: actions/checkout@v4
run: |
rm -rf ./* ./.[!.]* 2>/dev/null || true
git config --global http.extraheader "Authorization: token ${{ github.token }}"
git clone --branch "${{ gitea.ref_name }}" \
"${{ gitea.server_url }}/${{ gitea.repository }}.git" .
- name: Setup Node
uses: actions/setup-node@v4
uses: https://github.com/actions/setup-node@v4
with:
node-version: 24
+3
View File
@@ -7,3 +7,6 @@ bun.lockb
# Miscellaneous
/static/
# Generated code
/src/lib/generated/
-27
View File
@@ -1,27 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Backend (server-side code)",
"type": "node",
"request": "launch",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"skipFiles": ["<node_internals>/**"],
"autoAttachChildProcesses": true
},
{
"name": "Frontend (client-side code)",
"type": "chrome",
"request": "launch",
"preLaunchTask": "Start Dev Server",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}",
"sourceMapPathOverrides": {
"vite:///src/*": "${webRoot}/src/*"
}
}
]
}
-1
View File
@@ -1 +0,0 @@
{}
-26
View File
@@ -1,26 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Start Dev Server",
"type": "shell",
"command": "npm run dev -- --port 5173 --strictPort",
"isBackground": true,
"problemMatcher": {
"pattern": [
{
"regexp": "^.*$",
"file": 0,
"location": 0,
"message": 0
}
],
"background": {
"activeOnStart": true,
"beginsPattern": "^.*VITE.*v",
"endsPattern": "^.*localhost:.*$"
}
}
}
]
}
+30 -33
View File
@@ -9,11 +9,11 @@ AGENTS.md
at your project root:
````text
# Rhemawave Portal AI Guide
# Radiant Wave Portal AI Guide
## Mission
You are working on portal.Rhemawavetech.com.
You are working on portal.radiantwavetech.com.
Act as a senior engineer making complete, production-ready changes.
@@ -97,9 +97,9 @@ Use narrower commands when appropriate.
If validation fails:
* Diagnose the failure.
* Fix the issue.
* Run validation again.
- Diagnose the failure.
- Fix the issue.
- Run validation again.
Never finish with known failures.
@@ -109,19 +109,19 @@ Never finish with known failures.
Application:
portal.Rhemawavetech.com
portal.radiantwavetech.com
Stack:
* SvelteKit 2
* Svelte 5
* TypeScript
* Tailwind CSS v4
* Prisma 7
* PostgreSQL
* Better Auth
* Vitest
* Playwright
- SvelteKit 2
- Svelte 5
- TypeScript
- Tailwind CSS v4
- Prisma 7
- PostgreSQL
- Better Auth
- Vitest
- Playwright
Package manager:
@@ -266,9 +266,9 @@ AdminPage.svelte
Registry metadata exists for:
* columns
* searches
* relations
- columns
- searches
- relations
Forms are manually implemented per route.
@@ -309,8 +309,8 @@ Only load that file when modifying Svelte components or Svelte logic.
Testing uses:
* Vitest
* Playwright browser testing
- Vitest
- Playwright browser testing
For testing patterns, read:
@@ -338,10 +338,10 @@ Only load that file when working on API functionality.
Prettier rules:
* tabs
* single quotes
* no trailing commas
* 80 character print width
- tabs
- single quotes
- no trailing commas
- 80 character print width
Follow existing formatting.
@@ -391,17 +391,14 @@ Confirm:
2. Relevant tests/checks were run.
3. Failures were fixed.
4. The final response explains:
* what changed
* files modified
* verification performed
- what changed
- files modified
- verification performed
Do not stop at:
* "I found the issue"
* "Here is a plan"
* "The next step would be"
- "I found the issue"
- "Here is a plan"
- "The next step would be"
Finish the work.
+2 -2
View File
@@ -1,4 +1,4 @@
# portal.Rhemawavetech.com
# portal.radiantwavetech.com
SvelteKit 2 + Svelte 5 (runes) + Tailwind CSS v4 + Prisma 7 + PostgreSQL + Better-Auth.
@@ -332,7 +332,7 @@ The `$effect` resets `dirty = false` on mount (when the record prop changes).
| Template ID | Variables | Subject (API-level) |
| ------------------------------ | --------------------------------------------------------------- | ------------------------------ |
| `automated-user-registration` | `firstName`, `tempPassword`, `verifyUrl`, `loginUrl` | "Your account is ready" |
| `license-key-delivery` | `firstName`, `license_key` | "Your Rhemawave License Key" |
| `license-key-delivery` | `firstName`, `license_key` | "Your Radiant Wave License Key" |
| `email-verification-email` | `target_email_address`, `verification_link` | "Verify your email address" |
Variables must be declared in the Resend template editor's Variables section. Undeclared variables render as literal `{{{varName}}}` text.
+14 -15
View File
@@ -1,4 +1,4 @@
# Rhema Wave Technologies - User Portal
# Radiant Wave Technologies - User Portal
A combination application that combines the main Radiant Wave API with a user-facing account management application,
and an admin-facing platform management application. An all-in-one management platform for Radiant Wave and Radiant Wave Technologies.
@@ -36,18 +36,17 @@ podman push registry.digitalocean.com/radiantwave-portal-container-registry/port
## Admin
Admin CRUD pages at `/admin`. 9 models have per-page routes with modals for create/edit:
Admin pages live under `/admin` (requires `roleId <= 3`); `/admin` redirects to
`/admin/profiles`. Each route owns its own form and modal logic, built on the shared
`DataTable.svelte` and `AdminModal.svelte` components. The generic `$lib/svelte-admin`
CRUD layer has been removed; see `docs/ADMIN_OVERHAUL.md` for the migration notes.
| Included | Excluded |
| ------------------------ | --------------------------------- |
| users | Session (Better Auth) |
| profiles | Account (Better Auth) |
| addresses | Verification (Better Auth) |
| user-roles | UserProclamations (composite key) |
| proclamations | |
| solfeggio-frequencies | |
| proclamation-categories | |
| license-keys (list-only) | |
| subscription-levels | |
See `src/lib/svelte-admin/README.md` for architecture and configuration docs.
| Route | Notes |
| -------------------------------- | ------------------------ |
| `/admin/users` | Name, Email, Role |
| `/admin/profiles` | Reference implementation |
| `/admin/proclamations` | Super-only content edits |
| `/admin/solfeggio-frequencies` | |
| `/admin/proclamation-categories` | Draggable reorder |
| `/admin/proclamation-groups` | Draggable reorder |
| `/admin/license-keys` | List-only + rotate |
+22 -4
View File
@@ -89,14 +89,34 @@ POST:
Payload:
{
"license_key": "...",
"email_address": "..."
"license_key": "...",
"email_address": "..."
}
Requires API key.
---
# Schedule (v2)
Endpoint:
GET /api/v2/schedule?email=<email>&licenseKey=<key>
Authenticates with the paired `email` + `licenseKey` query parameters and
validates only that the key belongs to the account (not that the license is
active/entitled). Returns the user's Blocks with start/end times and the IDs of
the selected Proclamations (content is fetched separately via
`/api/v2/proclamations`). Each block also carries `dynamicAudio`; when true, the
kiosk enables dynamic audio for the block's duration, overriding any audio
selections made on the device while the block is active. Overnight blocks (end
clock time before start) are supported and flagged with `overnight: true` plus a
wrapping `durationMinutes`. Rate limited to 10 requests per minute per IP.
Failure messages follow the existing v2 shape:
`not-authorized` or `this key doesn't appear to belong to you`.
---
# GHL Account Creation Webhook
Endpoint:
@@ -153,7 +173,6 @@ Variables:
- verifyUrl
- loginUrl
license-key-delivery
Variables:
@@ -161,7 +180,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().
+59
View File
@@ -0,0 +1,59 @@
{
"version": "sched-adjacent-0001",
"type": "daily",
"daily": [
{
"id": "dd111111-1111-4111-8111-111111111111",
"startTime": "06:00",
"startMinutes": 360,
"endTime": "07:00",
"endMinutes": 420,
"overnight": false,
"durationMinutes": 60,
"proclamationIds": ["b40fdcb9-703c-420e-af32-a081964c6f08"],
"dynamicAudio": true
},
{
"id": "dd222222-2222-4222-8222-222222222222",
"startTime": "07:00",
"startMinutes": 420,
"endTime": "08:30",
"endMinutes": 510,
"overnight": false,
"durationMinutes": 90,
"proclamationIds": [
"7c9e1a2b-3d4e-5f60-8a9b-0c1d2e3f4a5b",
"33333333-3333-4333-8333-333333333333"
],
"dynamicAudio": false
}
],
"weekly": {},
"blocks": [
{
"id": "dd111111-1111-4111-8111-111111111111",
"startTime": "06:00",
"startMinutes": 360,
"endTime": "07:00",
"endMinutes": 420,
"overnight": false,
"durationMinutes": 60,
"dynamicAudio": true,
"proclamationIds": ["b40fdcb9-703c-420e-af32-a081964c6f08"]
},
{
"id": "dd222222-2222-4222-8222-222222222222",
"startTime": "07:00",
"startMinutes": 420,
"endTime": "08:30",
"endMinutes": 510,
"overnight": false,
"durationMinutes": 90,
"dynamicAudio": false,
"proclamationIds": [
"7c9e1a2b-3d4e-5f60-8a9b-0c1d2e3f4a5b",
"33333333-3333-4333-8333-333333333333"
]
}
]
}
+59
View File
@@ -0,0 +1,59 @@
{
"version": "sched-daily-0001",
"type": "daily",
"daily": [
{
"id": "11111111-1111-4111-8111-111111111111",
"startTime": "07:00",
"startMinutes": 420,
"endTime": "09:00",
"endMinutes": 540,
"overnight": false,
"durationMinutes": 120,
"proclamationIds": [
"b40fdcb9-703c-420e-af32-a081964c6f08",
"7c9e1a2b-3d4e-5f60-8a9b-0c1d2e3f4a5b"
],
"dynamicAudio": true
},
{
"id": "22222222-2222-4222-8222-222222222222",
"startTime": "18:00",
"startMinutes": 1080,
"endTime": "20:30",
"endMinutes": 1230,
"overnight": false,
"durationMinutes": 150,
"proclamationIds": ["33333333-3333-4333-8333-333333333333"],
"dynamicAudio": false
}
],
"weekly": {},
"blocks": [
{
"id": "11111111-1111-4111-8111-111111111111",
"startTime": "07:00",
"startMinutes": 420,
"endTime": "09:00",
"endMinutes": 540,
"overnight": false,
"durationMinutes": 120,
"dynamicAudio": true,
"proclamationIds": [
"b40fdcb9-703c-420e-af32-a081964c6f08",
"7c9e1a2b-3d4e-5f60-8a9b-0c1d2e3f4a5b"
]
},
{
"id": "22222222-2222-4222-8222-222222222222",
"startTime": "18:00",
"startMinutes": 1080,
"endTime": "20:30",
"endMinutes": 1230,
"overnight": false,
"durationMinutes": 150,
"dynamicAudio": false,
"proclamationIds": ["33333333-3333-4333-8333-333333333333"]
}
]
}
+53
View File
@@ -0,0 +1,53 @@
{
"version": "sched-dynamic-0001",
"type": "daily",
"daily": [
{
"id": "ee111111-1111-4111-8111-111111111111",
"startTime": "08:00",
"startMinutes": 480,
"endTime": "10:00",
"endMinutes": 600,
"overnight": false,
"durationMinutes": 120,
"proclamationIds": ["b40fdcb9-703c-420e-af32-a081964c6f08"],
"dynamicAudio": true
},
{
"id": "ee222222-2222-4222-8222-222222222222",
"startTime": "20:00",
"startMinutes": 1200,
"endTime": "22:00",
"endMinutes": 1320,
"overnight": false,
"durationMinutes": 120,
"proclamationIds": ["7c9e1a2b-3d4e-5f60-8a9b-0c1d2e3f4a5b"],
"dynamicAudio": false
}
],
"weekly": {},
"blocks": [
{
"id": "ee111111-1111-4111-8111-111111111111",
"startTime": "08:00",
"startMinutes": 480,
"endTime": "10:00",
"endMinutes": 600,
"overnight": false,
"durationMinutes": 120,
"dynamicAudio": true,
"proclamationIds": ["b40fdcb9-703c-420e-af32-a081964c6f08"]
},
{
"id": "ee222222-2222-4222-8222-222222222222",
"startTime": "20:00",
"startMinutes": 1200,
"endTime": "22:00",
"endMinutes": 1320,
"overnight": false,
"durationMinutes": 120,
"dynamicAudio": false,
"proclamationIds": ["7c9e1a2b-3d4e-5f60-8a9b-0c1d2e3f4a5b"]
}
]
}
+30
View File
@@ -0,0 +1,30 @@
{
"email": "jane@example.com",
"blocks": [
{
"id": "11111111-1111-4111-8111-111111111111",
"startTime": "07:00",
"startMinutes": 420,
"endTime": "09:00",
"endMinutes": 540,
"overnight": false,
"durationMinutes": 120,
"dynamicAudio": true,
"proclamationIds": [
"b40fdcb9-703c-420e-af32-a081964c6f08",
"7c9e1a2b-3d4e-5f60-8a9b-0c1d2e3f4a5b"
]
},
{
"id": "22222222-2222-4222-8222-222222222222",
"startTime": "18:00",
"startMinutes": 1080,
"endTime": "20:30",
"endMinutes": 1230,
"overnight": false,
"durationMinutes": 150,
"dynamicAudio": false,
"proclamationIds": ["33333333-3333-4333-8333-333333333333"]
}
]
}
@@ -0,0 +1,59 @@
{
"version": "sched-midnight-0001",
"type": "daily",
"daily": [
{
"id": "cc111111-1111-4111-8111-111111111111",
"startTime": "21:00",
"startMinutes": 1260,
"endTime": "09:00",
"endMinutes": 540,
"overnight": true,
"durationMinutes": 720,
"proclamationIds": [
"b40fdcb9-703c-420e-af32-a081964c6f08",
"7c9e1a2b-3d4e-5f60-8a9b-0c1d2e3f4a5b"
],
"dynamicAudio": true
},
{
"id": "cc222222-2222-4222-8222-222222222222",
"startTime": "09:00",
"startMinutes": 540,
"endTime": "21:00",
"endMinutes": 1260,
"overnight": false,
"durationMinutes": 720,
"proclamationIds": ["33333333-3333-4333-8333-333333333333"],
"dynamicAudio": false
}
],
"weekly": {},
"blocks": [
{
"id": "cc111111-1111-4111-8111-111111111111",
"startTime": "21:00",
"startMinutes": 1260,
"endTime": "09:00",
"endMinutes": 540,
"overnight": true,
"durationMinutes": 720,
"dynamicAudio": true,
"proclamationIds": [
"b40fdcb9-703c-420e-af32-a081964c6f08",
"7c9e1a2b-3d4e-5f60-8a9b-0c1d2e3f4a5b"
]
},
{
"id": "cc222222-2222-4222-8222-222222222222",
"startTime": "09:00",
"startMinutes": 540,
"endTime": "21:00",
"endMinutes": 1260,
"overnight": false,
"durationMinutes": 720,
"dynamicAudio": false,
"proclamationIds": ["33333333-3333-4333-8333-333333333333"]
}
]
}
+7
View File
@@ -0,0 +1,7 @@
{
"version": "sched-none-0001",
"type": null,
"daily": [],
"weekly": {},
"blocks": []
}
+53
View File
@@ -0,0 +1,53 @@
{
"version": "7f53efde17d291a1ecf604e266d3803d1495b986",
"type": "daily",
"daily": [
{
"id": "6a9be0a4-4a1c-4a2c-929d-7b6c6fe56d91",
"startTime": "06:00",
"startMinutes": 360,
"endTime": "07:00",
"endMinutes": 420,
"overnight": false,
"durationMinutes": 60,
"dynamicAudio": true,
"proclamationIds": ["310138f3-adfc-45ce-ae52-07279cd25b48"]
},
{
"id": "65fecafc-abc8-4796-bb12-ed8e9d99e0fe",
"startTime": "18:00",
"startMinutes": 1080,
"endTime": "20:30",
"endMinutes": 1230,
"overnight": false,
"durationMinutes": 150,
"dynamicAudio": false,
"proclamationIds": ["3805242e-4930-4dd2-afb1-130593b1dbab"]
}
],
"weekly": {},
"blocks": [
{
"id": "6a9be0a4-4a1c-4a2c-929d-7b6c6fe56d91",
"startTime": "06:00",
"startMinutes": 360,
"endTime": "07:00",
"endMinutes": 420,
"overnight": false,
"durationMinutes": 60,
"dynamicAudio": true,
"proclamationIds": ["310138f3-adfc-45ce-ae52-07279cd25b48"]
},
{
"id": "65fecafc-abc8-4796-bb12-ed8e9d99e0fe",
"startTime": "18:00",
"startMinutes": 1080,
"endTime": "20:30",
"endMinutes": 1230,
"overnight": false,
"durationMinutes": 150,
"dynamicAudio": false,
"proclamationIds": ["3805242e-4930-4dd2-afb1-130593b1dbab"]
}
]
}
+50
View File
@@ -0,0 +1,50 @@
{
"version": "6de852ba72ce1ab3052150640730ee87a7d460b5",
"type": "weekly",
"daily": [],
"weekly": {
"mon": [
{
"id": "a1054b78-480a-4e87-8465-aac8d677c666",
"startTime": "06:00",
"startMinutes": 360,
"endTime": "07:00",
"endMinutes": 420,
"overnight": false,
"durationMinutes": 60,
"dynamicAudio": true,
"proclamationIds": ["310138f3-adfc-45ce-ae52-07279cd25b48"]
},
{
"id": "25979ec9-f7da-4aa4-864f-f74b1a2da78b",
"startTime": "07:00",
"startMinutes": 420,
"endTime": "08:30",
"endMinutes": 510,
"overnight": false,
"durationMinutes": 90,
"dynamicAudio": true,
"proclamationIds": ["3805242e-4930-4dd2-afb1-130593b1dbab"]
}
],
"tue": [],
"wed": [],
"thu": [],
"fri": [],
"sat": [],
"sun": [
{
"id": "e2237d98-834c-433b-8979-b56ddad2ef7a",
"startTime": "23:00",
"startMinutes": 1380,
"endTime": "01:00",
"endMinutes": 60,
"overnight": true,
"durationMinutes": 120,
"dynamicAudio": true,
"proclamationIds": ["fcd5a291-3d6e-4678-9d85-9b550e80f1c7"]
}
]
},
"blocks": []
}
+63
View File
@@ -0,0 +1,63 @@
{
"version": "sched-weekly-0001",
"type": "weekly",
"daily": [],
"weekly": {
"mon": [
{
"id": "a1111111-1111-4111-8111-111111111111",
"startTime": "06:00",
"startMinutes": 360,
"endTime": "08:00",
"endMinutes": 480,
"overnight": false,
"durationMinutes": 120,
"proclamationIds": ["b40fdcb9-703c-420e-af32-a081964c6f08"],
"dynamicAudio": true
}
],
"tue": [],
"wed": [
{
"id": "a2222222-2222-4222-8222-222222222222",
"startTime": "12:00",
"startMinutes": 720,
"endTime": "13:00",
"endMinutes": 780,
"overnight": false,
"durationMinutes": 60,
"proclamationIds": ["7c9e1a2b-3d4e-5f60-8a9b-0c1d2e3f4a5b"],
"dynamicAudio": true
}
],
"thu": [],
"fri": [
{
"id": "a3333333-3333-4333-8333-333333333333",
"startTime": "18:00",
"startMinutes": 1080,
"endTime": "22:00",
"endMinutes": 1320,
"overnight": false,
"durationMinutes": 240,
"proclamationIds": ["33333333-3333-4333-8333-333333333333"],
"dynamicAudio": false
}
],
"sat": [],
"sun": [
{
"id": "a4444444-4444-4444-8444-444444444444",
"startTime": "23:00",
"startMinutes": 1380,
"endTime": "01:00",
"endMinutes": 60,
"overnight": true,
"durationMinutes": 120,
"proclamationIds": ["b40fdcb9-703c-420e-af32-a081964c6f08"],
"dynamicAudio": true
}
]
},
"blocks": []
}
+802
View File
@@ -0,0 +1,802 @@
openapi: 3.0.3
info:
title: Radiant Wave Portal API (v2)
version: 2.0.0
description: |
Read API used by the Radiant Wave kiosk plus the automation onboarding
webhook.
## Authentication
The v2 API is **not** API-key gated. The former bearer-token middleware in
`src/hooks.server.ts` is present but disabled, so v2 routes are served
without a global API key.
Two endpoints carry their own credential semantics:
- `GET /api/v2/proclamations` optionally accepts the paired
`x-license-key` / `x-email` headers to scope results to an entitled user.
Supplying neither returns the full catalogue; supplying only one, or a
pair that does not match, is rejected.
- `POST /api/v2/automations/create/account` is protected by an HMAC-SHA256
request signature (see the operation description).
servers:
- url: https://portal.radiantwavetech.com
description: Production
- url: http://localhost:5173
description: Local development
tags:
- name: Catalogue
description: Reference data for categories, groups, frequencies and palettes.
- name: Proclamations
description: Proclamation content, optionally scoped by license entitlement.
- name: License
description: License key validation.
- name: Schedule
description: A user's daily listening schedule, consumed by the kiosk.
- name: Automations
description: Signed machine-to-machine webhooks.
paths:
/api/v2/categories:
get:
tags: [Catalogue]
summary: List proclamation categories
operationId: listCategories
responses:
'200':
description: All proclamation categories.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Category'
/api/v2/proclamation-categories:
get:
tags: [Catalogue]
summary: List proclamation categories (dedicated alias)
description: Alias of `/api/v2/categories`; returns the identical payload.
operationId: listProclamationCategories
responses:
'200':
description: All proclamation categories.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Category'
/api/v2/groups:
get:
tags: [Catalogue]
summary: List proclamation groups
description: Groups are ordered by `sortOrder` ascending.
operationId: listGroups
responses:
'200':
description: All proclamation groups.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Group'
/api/v2/proclamation-groups:
get:
tags: [Catalogue]
summary: List proclamation groups (dedicated alias)
description: Alias of `/api/v2/groups`; returns the identical payload.
operationId: listProclamationGroups
responses:
'200':
description: All proclamation groups.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Group'
/api/v2/frequencies:
get:
tags: [Catalogue]
summary: List canonical solfeggio frequencies
description: |
Returns the nine canonical frequencies (ids 1-9) the kiosk expects.
These are static values, not the portal database autoincrement ids.
operationId: listFrequencies
responses:
'200':
description: The nine canonical frequencies.
content:
application/json:
schema:
type: array
minItems: 9
maxItems: 9
items:
$ref: '#/components/schemas/Frequency'
/api/v2/color-palettes:
get:
tags: [Catalogue]
summary: List color palettes per canonical frequency
description: |
Returns one 7-color palette for each canonical frequency id (1-9).
Palette rows stored in the database take precedence; ids without a row
fall back to the built-in canonical palette.
operationId: listColorPalettes
responses:
'200':
description: Nine palettes, one per canonical frequency id.
content:
application/json:
schema:
type: array
minItems: 9
maxItems: 9
items:
$ref: '#/components/schemas/ColorPalette'
/api/v2/proclamations:
get:
tags: [Proclamations]
summary: List proclamations
description: |
Without entitlement headers, returns the full proclamation catalogue.
With both `x-license-key` and `x-email`, returns only the proclamations
assigned to the matching user. Both headers must be present together and
the key must belong to the user.
Proclamations whose frequency has no canonical id (for example 936 Hz)
are omitted from the response.
operationId: listProclamations
parameters:
- in: header
name: x-license-key
schema:
type: string
required: false
description: License key. Must be supplied together with `x-email`.
- in: header
name: x-email
schema:
type: string
format: email
required: false
description: Account email. Must be supplied together with `x-license-key`.
responses:
'200':
description: Proclamation records with canonical frequency ids and embedded palettes.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Proclamation'
'400':
description: |
Missing entitlement or the supplied key/email pair is invalid.
Message is `not-authorized`, or
`this key doesn't appear to belong to you` when the key does not
belong to the specified user.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
/api/v2/license:
post:
tags: [License]
summary: Validate a license key
description: |
Validates that the license key exists, is active, and belongs to the
account matching `email_address`. A successful validation records
`lastValidatedAt` on the key.
operationId: validateLicense
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LicenseRequest'
responses:
'200':
description: The validated key and its owner email.
content:
application/json:
schema:
$ref: '#/components/schemas/LicenseResponse'
'400':
description: |
Validation failure or malformed body. Possible messages:
`content-type must be application/json`, `invalid JSON body`,
`request body is required`, `license_key is required`,
`email_address is required`, `license key not found`,
`license key is not active`, `user does not exist`,
`key does not belong to specified user`.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
'413':
description: The request body exceeded the 64 KiB limit.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
'429':
description: Rate limit exceeded (10 requests per minute per IP).
headers:
Retry-After:
schema:
type: integer
description: Seconds to wait before retrying.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
/api/v2/schedule:
get:
tags: [Schedule]
summary: Get a user's listening schedule (query-param auth)
description: |
Machine-to-app endpoint used by the RadiantWave kiosk. Authenticates via
the paired `email` + `licenseKey` **query parameters** and validates only
that the key belongs to the account for that email. License
active/entitlement is deliberately not checked here: an invalid license
would already lock the kiosk out, so this endpoint only checks the
pairing.
Returns every Block with its start/end times and the ordered list of
selected Proclamation IDs (the legacy daily flattening in `blocks`), and
is extended additively with `version`, `type`, `daily`, and `weekly`.
Overnight blocks (an end clock time before the start) are supported and
flagged with `overnight: true` and a wrapping `durationMinutes`. Each
block also carries `dynamicAudio`; when true, the kiosk enables dynamic
audio for the block's duration, overriding any audio selections made on
the device. Proclamation content is not included; the kiosk fetches it
separately via `/api/v2/proclamations`. For `type: "weekly"`, `blocks` is
an empty array (the server cannot flatten weekly in the kiosk's
timezone). This endpoint emits no ETag and never returns 304.
operationId: getSchedule
parameters:
- in: query
name: email
required: true
schema:
type: string
format: email
description: Account email.
- in: query
name: licenseKey
required: true
schema:
type: string
pattern: '^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$'
description: License key. The legacy alias `key` is also accepted.
responses:
'200':
description: The user's schedule (possibly empty).
content:
application/json:
schema:
$ref: '#/components/schemas/ScheduleResponse'
example:
email: jane@example.com
blocks:
- id: 3f1c2b7a-9d4e-4c8a-b2f6-1a2b3c4d5e6f
startTime: '21:00'
startMinutes: 1260
endTime: '09:00'
endMinutes: 540
overnight: true
durationMinutes: 720
dynamicAudio: true
proclamationIds:
- b40fdcb9-703c-420e-af32-a081964c6f08
- 7c9e1a2b-3d4e-5f60-8a9b-0c1d2e3f4a5b
'400':
description: |
Missing/invalid parameters, unknown key or user, or the key does not
belong to the email's account. Possible messages: `not-authorized`,
`this key doesn't appear to belong to you`.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
'429':
description: Rate limit exceeded (10 requests per minute per IP).
headers:
Retry-After:
schema:
type: integer
description: Seconds to wait before retrying.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
/api/v2/licenses/{licenseKey}/schedule:
get:
tags: [Schedule]
summary: Get a License Key's schedule (Kiosk-facing, conditional)
description: |
Kiosk-facing, License-Key-only schedule read. Returns the target payload
(`version`, `type`, `daily`, `weekly`, `blocks`). `version` is a
content-derived opaque string and is also the `ETag`; a matching
`If-None-Match` returns `304`. An unknown License Key is `404`; a key
with no Schedule returns `type: null` with empty lists. No
authentication (see the API description) beyond the 10/min/IP limit.
operationId: getLicenseSchedule
parameters:
- in: path
name: licenseKey
required: true
schema:
type: string
pattern: '^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$'
- in: header
name: If-None-Match
required: false
schema:
type: string
description: Prior ETag; when it matches, returns 304.
responses:
'200':
description: The target schedule payload.
headers:
ETag:
schema:
type: string
description: The quoted `version`.
content:
application/json:
schema:
$ref: '#/components/schemas/SchedulePayload'
'304':
description: Not modified; the caller keeps its cached payload.
headers:
ETag:
schema:
type: string
'404':
description: 'Unknown license (`unknown_license`).'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
'429':
description: Rate limit exceeded (10 requests per minute per IP).
headers:
Retry-After:
schema:
type: integer
description: Seconds to wait before retrying.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
/api/v2/licenses/{licenseKey}/system-name:
put:
tags: [License]
summary: Set a License Key's System Name (Kiosk-only)
description: |
Sets the human-readable System Name for a License Key. The Kiosk is the
only writer; the Portal displays the name read-only. The name is unique
per account, compared case-insensitively, when non-null. Idempotent:
re-sending the current name returns 200 without a write.
operationId: setSystemName
parameters:
- in: path
name: licenseKey
required: true
schema:
type: string
pattern: '^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$'
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [systemName]
properties:
systemName:
type: string
minLength: 1
maxLength: 40
responses:
'200':
description: The stored name.
content:
application/json:
schema:
type: object
required: [systemName, systemNameStatus]
properties:
systemName:
type: string
systemNameStatus:
type: string
enum: [ok]
'400':
description: 'Invalid name (`invalid_name`); empty/whitespace is invalid.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
'404':
description: 'Unknown license (`unknown_license`).'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
'409':
description: 'Name already used on the account (`name_taken`).'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
'429':
description: Rate limit exceeded (10 requests per minute per IP).
headers:
Retry-After:
schema:
type: integer
description: Seconds to wait before retrying.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
/api/v2/automations/create/account:
post:
tags: [Automations]
summary: Create an account and license key (signed webhook)
description: |
Machine-to-machine onboarding webhook. The request must be signed with
HMAC-SHA256 using `API_V2_ONBOARDING_SECRET`.
The canonical string that is signed is the newline-joined concatenation
of:
```
POST
<path>
<x-timestamp>
<x-nonce>
<raw request body>
```
where `<path>` is `/api/v2/automations/create/account`. The signature is
the lowercase hex digest.
On success, creates the user if they do not already exist, generates a
license key, emails it, and returns the user id. Invalid or missing
signatures return `401 { "error": "invalid signature" }` (the same
response is used for every signature failure, including replay and
timestamp skew).
operationId: createAutomationAccount
parameters:
- in: header
name: x-timestamp
required: true
schema:
type: string
description: Unix epoch seconds. Must be within 300 seconds of the server clock.
- in: header
name: x-nonce
required: true
schema:
type: string
description: Unique value per request. Replays within the skew window are rejected.
- in: header
name: x-signature
required: true
schema:
type: string
pattern: '^[0-9a-f]{64}$'
description: Lowercase hex HMAC-SHA256 of the canonical request string.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/OnboardingRequest'
responses:
'200':
description: Account and license key created.
content:
application/json:
schema:
$ref: '#/components/schemas/OnboardingResponse'
'400':
description: |
Malformed or invalid request. Possible messages:
`request body is required`, `invalid JSON body`, or
`email is required`.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
'401':
description: Signature missing, malformed, expired, replayed, or incorrect.
content:
application/json:
schema:
type: object
required: [error]
properties:
error:
type: string
example: invalid signature
'413':
description: The request body exceeded the 64 KiB limit.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
'429':
description: |
Rate limit exceeded. Limits are 5 requests per minute per IP and
3 requests per 24 hours per email address.
headers:
Retry-After:
schema:
type: integer
description: Seconds to wait before retrying.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
'500':
description: The account could not be created.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
components:
schemas:
Category:
type: object
required: [id, name, sortOrder]
properties:
id:
type: integer
name:
type: string
sortOrder:
type: integer
Group:
type: object
required: [id, name, sortOrder]
properties:
id:
type: integer
name:
type: string
sortOrder:
type: integer
Frequency:
type: object
required: [id, name, value]
properties:
id:
type: integer
minimum: 1
maximum: 9
description: Canonical id used by the kiosk.
name:
type: string
value:
type: integer
description: Frequency in Hz.
ColorPalette:
type: object
required: [solfeggioFrequencyId, colors]
properties:
solfeggioFrequencyId:
type: integer
minimum: 1
maximum: 9
colors:
type: array
description: Seven hex colors, ordered light to dark.
minItems: 1
items:
type: string
pattern: '^#[0-9A-Fa-f]{6}$'
Proclamation:
type: object
required: [id, name, content, categoryId, groupId, solfeggioFrequencyId]
properties:
id:
type: string
format: uuid
name:
type: string
content:
type: string
description: HTML content.
categoryId:
type: integer
groupId:
type: integer
solfeggioFrequencyId:
type: integer
minimum: 1
maximum: 9
colorPalette:
type: array
description: Present only when a palette exists for the canonical frequency id.
minItems: 1
items:
type: string
pattern: '^#[0-9A-Fa-f]{6}$'
LicenseRequest:
type: object
required: [license_key, email_address]
properties:
license_key:
type: string
pattern: '^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$'
example: ABCD-EFGH-IJKL-MNOP
email_address:
type: string
format: email
maxLength: 254
systemName:
type: string
minLength: 1
maxLength: 40
description: |
Optional. The Kiosk's System Name (trimmed). Omitted means the stored
name is left untouched; empty/whitespace is invalid, never a clear.
LicenseResponse:
type: object
required: [key, email_address, systemNameStatus]
properties:
key:
type: string
example: ABCD-EFGH-IJKL-MNOP
email_address:
type: string
format: email
systemNameStatus:
type: string
enum: [ok, unchanged, conflict, invalid]
description: |
Advisory outcome of the optional `systemName`. Validation itself
always succeeds. `unchanged` is returned when the field is omitted.
ScheduleBlock:
type: object
required:
[
id,
startTime,
startMinutes,
endTime,
endMinutes,
overnight,
durationMinutes,
dynamicAudio,
proclamationIds
]
properties:
id:
type: string
format: uuid
startTime:
type: string
example: '06:00'
description: Wall-clock start time as `HH:MM`.
startMinutes:
type: integer
minimum: 0
maximum: 1440
description: Minutes from midnight.
endTime:
type: string
example: '08:00'
description: Wall-clock end time as `HH:MM`.
endMinutes:
type: integer
minimum: 0
maximum: 1440
description: Minutes from midnight.
overnight:
type: boolean
description: True when the end clock time is at or before the start, i.e. the block crosses midnight.
durationMinutes:
type: integer
minimum: 1
description: Block length in minutes. An overnight block counts the wrap past midnight as the next day.
dynamicAudio:
type: boolean
description: |
When true, dynamic audio is enabled for the block's duration, overriding any
audio selections made on the device while the block is active.
proclamationIds:
type: array
description: IDs of the selected Proclamations, in play order.
items:
type: string
format: uuid
ScheduleResponse:
type: object
required: [email, blocks]
properties:
email:
type: string
format: email
version:
type: string
type:
type: string
nullable: true
enum: [daily, weekly, null]
daily:
type: array
items:
$ref: '#/components/schemas/ScheduleBlock'
weekly:
type: object
additionalProperties:
type: array
items:
$ref: '#/components/schemas/ScheduleBlock'
blocks:
type: array
items:
$ref: '#/components/schemas/ScheduleBlock'
SchedulePayload:
type: object
required: [version, type, daily, weekly, blocks]
properties:
version:
type: string
description: Opaque content hash; doubles as the ETag.
type:
type: string
nullable: true
enum: [daily, weekly, null]
daily:
type: array
items:
$ref: '#/components/schemas/ScheduleBlock'
weekly:
type: object
description: Present with all seven keys when `type` is weekly.
additionalProperties:
type: array
items:
$ref: '#/components/schemas/ScheduleBlock'
blocks:
type: array
description: Legacy daily flattening; empty for weekly.
items:
$ref: '#/components/schemas/ScheduleBlock'
OnboardingRequest:
type: object
required: [email]
properties:
email:
type: string
format: email
maxLength: 254
name:
type: string
maxLength: 120
description: Optional display name. Defaults to the email local-part.
OnboardingResponse:
type: object
required: [success, message, user_id]
properties:
success:
type: boolean
message:
type: string
example: Account and license key created
user_id:
type: string
ErrorMessage:
type: object
required: [message]
properties:
message:
type: string
+619
View File
@@ -0,0 +1,619 @@
# Schedule API & Data Contract
Status: Phase 0 (contract only — no application code). This document is the
authoritative API/data contract for the multi-phase scheduler overhaul. It is
kept **byte-identical** in both repositories:
- `radiantwave` (C++/SDL3 Kiosk app) — `docs/schedule-api-contract.md`
- `portal` (SvelteKit/Node) — `docs/schedule-api-contract.md`
Any change is applied to both copies in the same step, with a dated line
appended to the [Changelog](#changelog).
Glossary terms (Kiosk, Portal, Proclamation, Category, Group, Block, Schedule,
License Key, System Name) are used with the meanings defined in the scheduler
overhaul brief.
---
## Current state
This section reflects the code as it exists at the start of Phase 0. It is
descriptive, not aspirational; the target behaviour is defined in the later
sections.
### Kiosk (`radiantwave`)
- **License validation.** `POST /api/v2/license` with body
`{"license_key": "...", "email_address": "..."}`. Implemented in
`src/network/api_client.cpp` (`validateLicense`, `parseLicenseJson`) and
consumed by `src/network/license_service.cpp` (`validateStoredLicense`,
`markLicenseVerified`, `isLicenseWithinGracePeriod`). A 2xx response is the
device's "subscribed" flag; a 7-day offline grace window is stored as
`license_last_verified` in `system_kv` (`src/db/db_keys.h`). Response body
currently read: `{key, email_address}`; additional fields are ignored.
- **Schedule fetch.** `GET /api/v2/schedule?email=<email>&licenseKey=<key>`
(`src/network/api_client.cpp`: `fetchSchedule`, `parseScheduleJson`;
`PATH_SCHEDULE = "/schedule"`). Response shape read by the Kiosk:
`{email, blocks: [{id, startMinutes, endMinutes, overnight, durationMinutes,
dynamicAudio, proclamationIds}]}`. `dynamicAudio` is optional on the wire and
defaults to `true` in `parseScheduleJson`. Missing/blank credentials cause an
early `false`.
- **Schedule persistence.** SQLite tables `schedule_blocks` and
`schedule_block_proclamations` (`src/db/database_manager.cpp:185`,
`replaceScheduleBlocks` at `:1105`, `getScheduleBlocks` at `:1169`). Full
replace; block `sort_order` and per-block proclamation `position` preserved.
No day-of-week column and no schedule `type`; the model is implicitly daily.
- **Schedule sync.** `syncScheduleFromApi` (`src/network/proclamation_sync.cpp:172`)
is invoked only as a **best-effort** side effect of
`syncProclamationsFromApi` (`:168`). There is no boot sync, no network-up
sync, no periodic timer, and no conditional GET (`If-None-Match`/ETag).
`syncProclamationsFromApi` is called from `Home::syncProclamationsIfEmpty`
(`src/home.cpp:461`) and `ProclamationOptionsPage::onPush`
(`src/proclamation_options_page.cpp:49`). There is no explicit atomic
schedule cache file; the SQLite database is the durable store.
- **Schedule playback.** `src/scroller_page.cpp` selects the active block by
comparing `timezone::currentLocalMinutes(cfg.timezone)`
(`src/timezone_helper.h`) against each block via
`scroller::minuteInScheduleBlock` (`src/scroller_streams.h:31`). Block
selection is daily-only (no day-of-week). An active block's ordered
`proclamation_ids` replace the local selection
(`src/scroller_streams.cpp:57`). `effectiveDynamicAudio()`
(`src/scroller_page.cpp:302`) forces dynamic audio when the active block has
`dynamic_audio == true`, otherwise the device-wide
`config.audio.dynamic_audio` applies. A block change is latched in
`scheduleChangePending_` and applied at the next proclamation boundary.
- **Settings persistence.** `~/.local/radiantwave/config.yaml` (YAML) via
`ConfigManager` (`src/config/config_manager.cpp`, `src/config/yaml_parser.cpp`,
schema in `src/config/app_config.h`): `timezone`, `audio.dynamic_audio`,
`proclamation.schedule_active` (Kiosk-local on/off toggle, default `false`).
Identity lives in SQLite `system_kv` (`email_address`, `license_key`,
`license_last_verified`). There is no System Name anywhere.
- **Conventions.** `API_BASE_URL = "https://portal.radiantwavetech.com/api/v2"`
(`src/network/api_client.h`). Kiosk API knowledge is mirrored in
`openapi.yaml` at the repo root.
### Portal (`portal`)
- **Framework/tooling.** SvelteKit 2 + Svelte 5 + TypeScript + Tailwind v4;
Prisma 7 over PostgreSQL; Vitest. Migrations: Prisma Migrate
(`prisma/schema.prisma`, `prisma/migrations/`, `npm run migrate`,
`npm run deploy`; config in `prisma.config.ts`). API routes under
`src/routes/api/v1/` (API-key gated) and `src/routes/api/v2/`
(unauthenticated). v2 helpers: `src/lib/server/v2-helpers.ts`
(`jsonOk`, `jsonError`, `readJsonBody`, `rateLimited`, `clientIp`); errors
are `{ "message": "<text>" }`.
- **License validation.** `POST /api/v2/license`
(`src/routes/api/v2/license/+server.ts`): validates `license_key` +
`email_address`, requires the key be active and owned by the account, stamps
`LicenseKey.lastValidatedAt`, returns `serializeLicense({key, emailAddress})`
(`src/lib/server/v2-serializers.ts`). A v1 variant exists
(`src/routes/api/v1/license/+server.ts`).
- **Schedule storage.** `Schedule` is **per user** (`userId String @unique`,
`prisma/schema.prisma:263`), daily-only, `0..4` `Block`s. `Block`
(`:275`) stores `startMinutes`/`endMinutes`, `dynamicAudio Boolean
@default(false)`, and ordered `BlockProclamations` (`sortOrder`). The schema
comment states overnight ranges are disallowed in v1, yet the serializer and
Kiosk both support `overnight` — a known inconsistency.
- **Schedule service/validation.** `src/lib/server/schedule.ts` (DB
orchestration: `getOrCreateSchedule`, `listBlocks`, `createBlock`,
`updateBlock`, `deleteBlock`) and the isomorphic
`src/lib/schedule-utils.ts` (`MAX_BLOCKS = 4`, `SNAP_MINUTES = 15`,
`validateBlock`, `findOverlap`, `isOvernight`, `blockDuration`,
`blockSegments`, `formatTime`, `toHm`, `snapMinutes`). Validation enforces a
4-block maximum on create, `startMinutes !== endMinutes`, at least one
Proclamation, and no overlaps (overnight-aware via linear segments).
- **Schedule read API.** `GET /api/v2/schedule`
(`src/routes/api/v2/schedule/+server.ts`): auth by paired `email` +
`licenseKey` (legacy alias `key`) query params; checks only that the key
belongs to the email's account (entitlement deliberately not checked); rate
limited 10/min/IP; returns `{email, blocks: [serializeScheduleBlock(...)]}`
where `serializeScheduleBlock` (`src/lib/server/v2-serializers.ts:165`) emits
`{id, startTime, startMinutes, endTime, endMinutes, overnight,
durationMinutes, dynamicAudio, proclamationIds}`.
- **Schedule UI.** Protected user route
`src/routes/(protected)/(user)/schedule/` (`+page.server.ts`,
`+page.svelte`) with `src/lib/components/schedule/DaySchedule.svelte` (single
24h day timeline, drag-to-create) and `BlockModal.svelte`. Dashboard
(`(protected)/(user)/dashboard/`) lists the user's License Keys. No System
Name is displayed or editable.
### Gaps between current state and target
- Schedule is per-user, not per-License-Key.
- No `daily`/`weekly` typing; no day-of-week storage or UI.
- No `version`/ETag, no conditional GET.
- Schedule GET requires `email` + `licenseKey`; there is no license-key-only
route, no `/api/v2/licenses/...` namespace, and no system-name route.
- `dynamicAudio` DB default is `false`; the target default for new Blocks is
`true`.
- No System Name field, validation, or display.
- Sync is opportunistic with no boot/network-up/5-minute cadence and no
atomic cache file.
- Portal stores no timezone (matches target) and shows no informational
geoip-derived timezone.
---
## Time semantics
- Times are **minute precision**, stored as 24-hour `"HH:MM"` strings
representing **local wall-clock** time. AM/PM is display-only.
- Numerators may also be carried as integer minutes-from-midnight
(`startMinutes`/`endMinutes`, `0..1440`) for the Kiosk; the string form is
canonical on the wire.
- A Block is the **half-open interval `[start, end)`**.
- A Block may start on the same minute the previous Block ends (`end 07:00`,
next `start 07:00` is valid and has no gap).
- A Block whose `end` is earlier than its `start` **crosses midnight** and
belongs to its start day. In weekly mode, Sunday's crossing Block spills into
Monday. `start == end` is **invalid** (zero- or full-length ambiguity).
- **Overlaps are invalid**, including spillover from a midnight-crossing Block
into the next day's Blocks (or, in daily mode, into the same list's early
Blocks). Touching boundaries are **not** overlaps.
- Gaps between Blocks are allowed. During a gap the Kiosk plays whatever
Proclamations are selected locally on the Kiosk.
- The Kiosk evaluates Schedules in its own Timezone setting
(`config.timezone`, IANA code). **The payload carries no timezone.** The
Portal may show the user's geoip-derived timezone informationally only.
---
## Schedule data model (JSON)
Each License Key has **at most one** Schedule. `type` is `"daily"` or
`"weekly"` and determines the mode; there is no separate global mode. **No**
Schedule means `type` is `null`.
Suggested shape from the brief, adapted to existing field names (`start`/`end`
→ `startTime`/`endTime`, retaining the existing `startMinutes`/`endMinutes`
integers for the Kiosk):
```json
{
"version": "<opaque string; also used as the ETag>",
"type": "daily" | "weekly" | null,
"daily": [ Block, ... ],
"weekly": { "mon": [Block], "tue": [Block], "wed": [Block],
"thu": [Block], "fri": [Block], "sat": [Block],
"sun": [Block] },
"blocks": [ LegacyBlock, ... ]
}
```
Block:
```json
{
"id": "3f1c2b7a-9d4e-4c8a-b2f6-1a2b3c4d5e6f",
"startTime": "21:00",
"startMinutes": 1260,
"endTime": "09:00",
"endMinutes": 540,
"overnight": true,
"durationMinutes": 720,
"proclamationIds": ["b40fdcb9-703c-420e-af32-a081964c6f08"],
"dynamicAudio": true
}
```
- `version` is an opaque string derived from the Schedule content (see
[Endpoints](#endpoints)). It is implementation-defined and must change
whenever the serialized schedule changes.
- **Only the fields for the active type are populated.** For `type: "daily"`,
`daily` is populated and `weekly` is omitted or empty. For `type: "weekly"`,
`weekly` is populated with all seven keys and `daily` is omitted or empty.
For `type: null`, both are empty/omitted.
- `weekly` always contains all seven lowercase keys `mon`..`sun`; a day with no
Blocks is an empty array.
- `blocks` is the **legacy flattening** retained for older Kiosks (see
[Backward compatibility](#backward-compatibility)). It uses the existing
`LegacyBlock` shape serialized by the current portal
(`{id, startTime, startMinutes, endTime, endMinutes, overnight,
durationMinutes, dynamicAudio, proclamationIds}`). For `type: "weekly"`,
`blocks` is an **empty array** (the server does not know the Kiosk's
timezone and cannot flatten weekly to a single day).
- `proclamationIds` is the ordered play list (order is significant);
duplicates are not meaningful and are de-duplicated by the Portal.
- `dynamicAudio` defaults to `true` for new Blocks created on the Portal.
- Proclamation **content** is never included; the Kiosk fetches content
separately via `/api/v2/proclamations`. Portal users only ever see
title/category/group.
---
## Validation rules
These are enforced by the Portal (authoritative) and defended against by the
Kiosk.
1. A Schedule belongs to exactly one License Key; a License Key has at most one
Schedule.
2. `type` is one of `"daily"`, `"weekly"`, or `null`.
3. A Block has `start` and `end` times; `start == end` is invalid.
4. `end < start` denotes a midnight-crossing Block belonging to its start day.
5. Blocks within a day (and the spillover of a midnight-crossing Block into the
following day) must not overlap. Touching boundaries are allowed.
6. Weekly modes apply rules 3–5 independently per day; a `sun` Block crossing
midnight must not overlap `mon` early Blocks.
7. A Block must contain at least one Proclamation. Empty Blocks are rejected
server-side. The Kiosk defensively **skips invalid Blocks** and treats them
as gaps.
8. Gaps are allowed; during a gap local Kiosk selection applies.
9. `dynamicAudio` is a boolean; absent means `true` for new Blocks.
10. Times are minute-precision `HH:MM` strings; the Portal snaps UI input to
`SNAP_MINUTES` (15) with a `MINUTE_STEP` (5) constraint from the existing
helpers.
11. The existing Portal cap of `MAX_BLOCKS = 4` per day list is **retained**
for Phase 0 (see [Open items](#open-items)).
The Kiosk must not crash on, and must not play, an invalid Block: it skips it.
---
## Endpoints
The API remains **unauthenticated** (no API keys) for v2. This is a known
limitation and is recorded under [Open items](#open-items).
All new endpoints live under the existing versioned namespace
`/api/v2/...` to preserve conventions. v1 routes are unchanged.
### (a) License validation — existing, additive
`POST /api/v2/license`
Request (existing fields unchanged; `systemName` additive and optional):
```json
{
"license_key": "ABCD-EFGH-IJKL-MNOP",
"email_address": "jane@example.com",
"systemName": "Living Room"
}
```
Response (existing fields unchanged; `systemNameStatus` additive):
```json
{ "key": "ABCD-EFGH-IJKL-MNOP", "email_address": "jane@example.com", "systemNameStatus": "ok" }
```
- `systemName` is optional. When present it is trimmed; when absent the stored
name is left untouched.
- `systemNameStatus` ∈ `{"ok", "unchanged", "conflict", "invalid"}`:
- `ok` — name was set or updated.
- `unchanged` — omitted, or identical after trim/case-insensitive compare.
- `conflict` — another License Key on the same account already holds the name.
- `invalid` — trimmed length outside 1–40.
- **Validation itself succeeds regardless of name status.** A non-`ok` status
never fails the license check; the Kiosk treats it as advisory.
- Errors and rate limiting follow the existing endpoint (400 messages,
413 body cap, 429 with `Retry-After`).
### (b) Set System Name — Kiosk-facing
`PUT /api/v2/licenses/{licenseKey}/system-name`
Request:
```json
{ "systemName": "Living Room" }
```
Responses:
| Status | Condition | Body |
| --------------------- | ----------------------------------------- | ------------------------------------------------- |
| 200 | Name set/updated (or unchanged) | `{"systemName": "...", "systemNameStatus": "ok"}` |
| 400 `invalid_name` | Trimmed length outside 1–40 | `{"message": "invalid_name"}` |
| 404 `unknown_license` | No License Key matches | `{"message": "unknown_license"}` |
| 409 `name_taken` | Another key on the account holds the name | `{"message": "name_taken"}` |
- The Kiosk is the **only** writer. The Portal has no rename capability.
- The response body uses the existing v2 error convention
(`{"message": "<code>"}`).
- Rate limited at the existing 10 requests/min/IP convention.
### (c) Get Schedule — Kiosk-facing, key-only, conditional
`GET /api/v2/licenses/{licenseKey}/schedule`
- 200 with `ETag: "<version>"` and the payload from
[Schedule data model](#schedule-data-model-json). A License Key with no
Schedule returns 200 with `type: null` and empty lists.
- `304 Not Modified` when the request's `If-None-Match` matches the current
`ETag`.
- `404` (`unknown_license`) when no License Key matches.
- `version` doubles as the ETag value; the header value is the quoted form.
- The response is the **target** payload (`version`, `type`, `daily`, `weekly`,
`blocks`).
- Rate limited at the existing 10 requests/min/IP convention.
### (d) Legacy schedule read — existing, evolved additively
`GET /api/v2/schedule?email=<email>&licenseKey=<key>` remains supported with
its existing auth semantics. Its response is extended **additively** with the
same `version`, `type`, `daily`, and `weekly` fields, while continuing to
populate `email` and `blocks` exactly as before. Old Kiosks ignore the new
fields; new Kiosks may read either endpoint. This endpoint does **not** emit an
ETag and does **not** return 304.
### (e) Portal-internal endpoints (not Kiosk-facing)
These are session-authenticated Portal actions; they are **not** part of the
Kiosk API and are not served by `/api/v2/...`.
- Schedule CRUD — implemented in Phase 3 as SvelteKit form actions on
`(protected)/(user)/schedule` (`?/setType`, `?/createBlock`, `?/updateBlock`,
`?/deleteBlock`), backed by `$lib/server/schedule.ts`.
- **Copy** — implemented in Phase 4 as the `?/copy` action plus
`copySchedule()` in `$lib/server/schedule.ts`. Copies one License Key's
Schedule to one or more other License Keys **by value, in a single database
transaction** (all targets succeed or none). The target `type` becomes the
source's; Block rows get fresh IDs; each block's `dynamicAudio` and ordered
Proclamation references are preserved. The source and every target must
belong to the caller's account; a target equal to the source, an empty
target list, or a source without a schedule are rejected. The resulting
schedule is run through the shared validator before commit. Because Block IDs
are part of the serialized payload, every target's content-derived
`version`/ETag changes, so its Kiosk refetches.
---
## Sync behavior
Target Kiosk sync (defaults; adjust only if the code forces it):
- The Kiosk pulls the Schedule:
- on boot,
- on network-up,
- every 5 minutes.
- Pulls use conditional GET (`If-None-Match` / ETag); a `304` leaves the cache
untouched.
- The last Schedule is cached to disk with an **atomic write** and used offline.
- The schedule on/off toggle is **Kiosk-local** (existing
`proclamation.schedule_active`) and is not synced.
- With no cache and no network, the Kiosk behaves as if schedules are off
(local Proclamation selection applies).
- During gaps, or when schedules are off, the local audio selection applies.
Current-state deltas (to be closed by later phases): no boot/network-up/5-minute
cadence, no ETag, no explicit atomic cache file. These are recorded under
[Touchpoints per phase](#touchpoints-per-phase).
Identity for sync is the License Key. The legacy schedule endpoint additionally
requires the paired email; the new endpoint uses the License Key alone.
---
## System Name rules
- A System Name is a human-readable label for a License Key (e.g.
"Living Room"). It is a **label only** — never used as a reference or key.
- Unique **per account**, compared **case-insensitively**.
- Trimmed, **1–40 characters**.
- Set or changed **only via the Kiosk**. The Portal displays it **read-only**
and has no rename capability.
- The Portal falls back to displaying the License Key when no name is set.
- A Kiosk that omits `systemName` never clears an existing name.
- Wire representations: request field `systemName`; status field
`systemNameStatus` ∈ `{"ok","unchanged","conflict","invalid"}` (see
[Endpoints](#endpoints)).
---
## Dynamic audio
- Each Block has a boolean `dynamicAudio`.
- The Portal defaults it to **`true` for new Blocks**.
- `true` ⇒ the Kiosk uses dynamic audio for that Block.
- `false` ⇒ the Kiosk uses its locally chosen audio.
- During gaps, or when schedules are off, the local audio selection applies
(existing `config.audio.dynamic_audio` / fixed track).
- Kiosk parsing defaults an absent `dynamicAudio` to `true`.
---
## Backward compatibility
All existing endpoints and payloads keep working for Kiosks already in the
field. New fields are **additive**; no v2 field is removed or renamed, and v1
is untouched.
- **Old Kiosk vs new Portal.** An old Kiosk calls
`POST /api/v2/license` (no `systemName`) and
`GET /api/v2/schedule?email&licenseKey`, reading `blocks` and ignoring
unknown fields. The Portal keeps both, keeps the `blocks` shape byte-
compatible, and adds `version`/`type`/`daily`/`weekly` as extra fields. The
old Kiosk is unaffected and ignores `systemNameStatus`.
- **Daily schedules.** For `type: "daily"`, `blocks` is the flattened daily
list; an old Kiosk behaves exactly as today.
- **Weekly schedules.** `blocks` is an **empty array**. An old Kiosk therefore
sees no schedule and falls back to local Proclamation/audio selection. It
cannot interpret weekly schedules; this is accepted and recorded under
[Open items](#open-items).
- **No schedule.** `type: null`, `blocks: []`; both old and new Kiosks fall
back to local selection.
- **Omission of `systemName`** by an old Kiosk never clears an existing name;
`systemNameStatus` is simply ignored.
- The new endpoints are additive; their absence on old Kiosks is irrelevant.
---
## Touchpoints per phase
File/module candidates each later phase will change. Phase numbering is assumed
from the brief (Phase 3 = Portal schedule CRUD, Phase 4 = Portal schedule copy)
and is recorded under [Open items](#open-items).
### Kiosk (`radiantwave`)
- **Phase 1 (Kiosk scheduler UI, storage, sync, playback)**
- `src/network/api_client.h` / `src/network/api_client.cpp` — parse
`version`/`type`/`daily`/`weekly`, ETag/`If-None-Match`, `PUT`
system-name, `systemNameStatus`.
- `src/network/proclamation_sync.h` / `.cpp` — boot/network-up/5-minute
schedule sync, conditional GET, atomic cache.
- `src/network/license_service.h` / `.cpp` — send `systemName`, handle
`systemNameStatus`.
- `src/db/db_keys.h` — System Name / ETag keys; `src/db/database_manager.h`
/ `.cpp` — schedule `type`/day-of-week storage, cache.
- `src/scroller_page.h` / `.cpp`, `src/scroller_streams.h` / `.cpp` —
day-of-week Block selection, spillover.
- `src/timezone_helper.h` — local day-of-week.
- `src/config/app_config.h`, `src/config/config_manager.cpp`,
`src/config/yaml_parser.cpp` — settings surface.
- New page(s) for setting the System Name; tests in `tests/` registered in
`CMakeLists.txt`.
### Portal (`portal`)
- **Phase 2 (System Name + license validation)**
- `prisma/schema.prisma` + migration — `LicenseKey.systemName` and
case-insensitive uniqueness.
- `src/routes/api/v2/license/+server.ts` — `systemName` /
`systemNameStatus`.
- `src/routes/api/v2/licenses/[licenseKey]/system-name/+server.ts` — new.
- `src/lib/server/v2-serializers.ts`, `src/lib/server/v2-helpers.ts`.
- Dashboard read-only display.
- **Phase 3 (Schedule CRUD: daily/weekly, per-License-Key)**
- `prisma/schema.prisma` + migration — Schedule per License Key, `type`,
day-of-week.
- `src/lib/server/schedule.ts`, `src/lib/schedule-utils.ts` — weekly,
per-license orchestration, cross-day validation.
- `src/lib/server/v2-serializers.ts` — `version`/`type`/`daily`/`weekly`.
- `src/routes/api/v2/licenses/[licenseKey]/schedule/+server.ts` — new
(ETag/304).
- `src/routes/api/v2/schedule/+server.ts` — additive evolution.
- `src/routes/(protected)/(user)/schedule/*`,
`src/lib/components/schedule/*` — weekly UI, per-license selection.
- Specs: `schedule.spec.ts`, `page.server.spec.ts`, `schedule-utils.spec.ts`.
- **Phase 4 (Schedule copy)**
- Copy service in `src/lib/server/schedule.ts`; copy route/action; UI.
- **All phases** — `docs/openapi.yaml`, this contract, `docs/fixtures/`.
---
## Open items
Recorded ambiguities, conservative choices, and remaining known limitations.
Items 1–21 were resolved or restated during Phases 1–4; items 22–24 are Phase 5
findings. Everything marked "Resolved" is settled for this overhaul.
1. **Phase numbering.** The brief names Phases 3 (CRUD) and 4 (copy) only;
Phase 1 (Kiosk) and Phase 2 (System Name) are assumed here.
2. **`blocks` for weekly.** Chosen as an empty array (server cannot know the
Kiosk timezone). Consequence: old Kiosks see weekly schedules as "no
schedule". Implemented in Phase 3.
3. **Block cap.** The existing `MAX_BLOCKS = 4` per day list is retained.
Phase 3 applies it **per day list** (each of the seven weekly days gets its
own 4-block budget). Whether the cap should be lower is unresolved.
4. **Field naming.** The brief's `start`/`end` were mapped to the existing
`startTime`/`endTime`; `startMinutes`/`endMinutes` retained for the Kiosk.
5. **`version` derivation.** Resolved in Phase 3: a SHA-1 content hash of the
serialized `{type, daily, weekly}` shape. Stable for equal content, changes
on any mutation.
6. **Unauthenticated key-only access.** The new endpoints authenticate by
License Key alone (per glossary). Anyone who knows a key could read the
schedule or set the name. Mitigated only by the 10/min/IP rate limit. Known
limitation.
7. **`dynamicAudio` default flip.** DB default changed `false` → `true` for
**new** Blocks in Phase 3; existing Blocks keep their stored values.
8. **Per-user → per-License-Key migration.** Resolved in Phase 3: each existing
per-user Schedule is mapped to that account's **oldest** License Key
(`createdAt ASC`), as a daily schedule. Schedules with no owning
user/license are retained with `licenseKeyId NULL` and reported via a
migration `NOTICE` (not dropped).
9. **Cross-day spillover validation.** Implemented in Phase 3: weekly
`sun`→`mon` and daily self-wrap spillover are rejected as overlaps; the
shared `validateSchedule` is authoritative and mirrored client-side.
10. **Account scope for name uniqueness.** "Per account" = the owning User.
Legacy License Keys with only an `email` and no `userId` are an edge case
(written without a per-account uniqueness check).
11. **Geoip timezone display.** Phase 3 shows an informational note that times
run in the **Kiosk's** timezone and uses the **browser-resolved** timezone
as a fallback; no geoip dependency (paid or external) was added. The
Portal still stores no timezone.
12. **ETag on the legacy endpoint.** The legacy `GET /api/v2/schedule` emits no
ETag and never returns 304, to preserve old behaviour.
13. **`systemNameStatus` on v1.** Not added; v1 is unchanged.
14. **Trim/case representation.** Names are trimmed; uniqueness is
case-insensitive; the stored casing is preserved as provided.
15. **Sync cadence.** Resolved in Phase 3: the Kiosk loads its on-disk cache on
boot, then pulls on boot, on a 5-minute timer, and on demand.
16. **Atomic cache file.** Resolved in Phase 3:
`~/.local/radiantwave/schedule_cache.json`, written atomically (temp file +
rename); a corrupt cache is ignored.
17. **`type` encoding for no schedule.** `type: null`; `daily`/`weekly` absent
or empty.
18. **Legacy alias.** `key` remains accepted as a `licenseKey` alias on the
legacy schedule endpoint.
19. **Kiosk local toggle.** `proclamation.schedule_enabled` in `config.yaml`,
default **on**, surfaced as System Settings → Schedule. Not synced. When
off, the engine plays local selections but the cache keeps updating. The
older `proclamation.schedule_active` flag is retained in config but is not
consulted by the Phase 3 engine.
20. **Copy semantics.** Copies are **independent, by value** snapshots: no link
is kept to the source, and editing the source later does not affect targets
(and vice versa). A copy replaces any existing target Schedule in full
(type and blocks) and requires explicit confirmation in the UI when a target
already has one. No "copy to all" shortcut and no scheduled/linked copies
(per the Phase 4 brief). The copied schedule is validated before commit; a
source schedule that fails validation is rejected rather than copied.
21. **Copy and Kiosk code.** Verified against the Phase 3 Kiosk engine/fixture
tests that a copied payload (new Block IDs, same type/times/audio/refs) is
parsed and evaluated identically to any other; **no Kiosk changes were
required**.
22. **Phase 3 migration down is lossy for new-model data.** The `up` migration
(`20260929130000_move_schedule_to_license_key`) is additive and safe. Its
`down` path is **reversible in structure but lossy in data**: the down SQL
drops `Schedule.licenseKeyId`/`type` and `Block.day`, then deletes
`Schedule` rows whose `userId IS NULL`. Any schedule created under the
per-License-Key model (which sets `licenseKeyId` and leaves `userId` NULL)
is therefore **deleted** by a down-migration. Consequences:
- Back up `Schedule`/`Block`/`BlockProclamations` before any down-migration.
- Rolling back past Phase 3 requires restoring that data from backup; it is
**not** a data-preserving rollback.
- Rollback guidance is in
[scheduler-overhaul-release-notes.md](scheduler-overhaul-release-notes.md).
23. **Kiosk UI/E2E on real hardware not run here.** The verification ran the
Portal and the Kiosk schedule logic (engine, client, cache) via the test
harness and real HTTP against a locally running Portal; the SDL Kiosk UI was
not driven interactively because the build environment has no display. See
the manual checklist in the release notes and the Phase 5 report.
24. **Unauthenticated Kiosk endpoints remain a known limitation** (restated from
item 6): the key-only schedule and system-name endpoints authenticate by
License Key alone, mitigated only by the 10/min/IP rate limit. Authentication
is out of scope for this overhaul.
---
## Changelog
- 2026-09-29 — Initial contract created (Phase 0). Documents current state,
target time semantics, schedule data model, validation rules, endpoints
(`/api/v2/licenses/...`, additive license-validation fields), sync behavior,
System Name rules, dynamic audio, backward compatibility, touchpoints,
open items, and fixtures.
- 2026-09-29 — Phase 3. Schedule is now per License Key with daily/weekly and
`Block.day`; the shared `validateSchedule` is authoritative; the key-only
`GET /api/v2/licenses/{licenseKey}/schedule` (ETag/304) ships and the legacy
endpoint is extended additively; the Portal editor is per-system with a
browser-fallback timezone note; the Kiosk adds the conditional client,
atomic cache, schedule engine, and a System Settings On/Off toggle. Open
items 2, 5, 7, 8, 9, 11, 15, and 16 are updated to reflect the changes.
- 2026-09-29 — Phase 4. Schedule copy is implemented as a Portal-internal
action (`?/copy`) backed by a single-transaction service that copies by
value with fresh Block IDs (so target ETags change), preserving `dynamicAudio`
and Proclamation order, scoped to the caller's account, and validated before
commit. Endpoint (e) is finalized; open items 20 and 21 record the copy
semantics and the Kiosk no-change verification.
- 2026-09-29 — Phase 5 (final). End-to-end verification against a locally
running Portal on a fresh database: schedule create/type/copy services and
the Kiosk-facing HTTP endpoints (validation with/without `systemName`,
system-name PUT, key-only schedule GET with ETag/304/404, legacy endpoint
parity, cross-account rejection) all pass; old-client compatibility confirmed;
migrations verified up/down (the down path is lossy, see open item 22). Adds
open items 22–24 (lossy down-migration, unrun UI-on-hardware checks, and the
restated unauthenticated-endpoint limitation) and the release notes.
+55
View File
@@ -0,0 +1,55 @@
# Scheduler Overhaul — Phase 5 Verification Results
Prepared during Phase 5 against a **locally running Portal on a fresh database**.
This records what was actually executed and observed. It is a companion to
`scheduler-overhaul-release-notes.md` (rollout steps) and
`schedule-api-contract.md` (the contract). Where a scenario could not be run
without real Kiosk hardware or an interactive display, the row is marked and the
equivalent automated coverage is cited.
Environment:
- Portal dev server (`npm run dev`) on `http://localhost:5173`, pointed at a
fresh scratch database (`rwportal_p5`) created and migrated for this phase.
- Kiosk schedule logic exercised through the C++ test harness
(`test_schedule_engine`) and the Kiosk API client's JSON parsing.
- No display/Xvfb available, so the SDL Kiosk UI was **not** driven
interactively; see the manual checklist in the release notes.
Legend: **PASS = executed**, **AUTO = verified by automated test** (not
interactively), **MANUAL = requires a human on hardware/display**.
| # | Scenario | Result | Evidence / notes |
| --- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| a | Kiosk shows AM/PM everywhere; System Settings contains Email, License Key, System Name, Schedule toggle, WiFi, Timezone, Date Format | AUTO | `test_date_format` (12h conversion), `test_settings_menu` (exactly the 7 entries incl. System Name + Schedule). UI rendering itself is MANUAL. |
| b | Validate a License Key, name the Kiosk, see the name read-only on the Portal | PASS + MANUAL | HTTP: validation without name → `200 unchanged`; with name → `200 ok`. Portal render is MANUAL (release-notes checklist). |
| c | Two systems in one account: duplicate name in any casing rejected and reverted on the Kiosk | PASS | HTTP: case-insensitive duplicate → validation `200 conflict`; `PUT` duplicate → `409 name_taken`; blank → `400 invalid_name`; unknown → `404 unknown_license`. Kiosk revert covered by `test_license_service`/`test_system_name`. |
| d | Rename a system on the Kiosk; its Schedule stays attached (License Key reference) | AUTO | Schedules are keyed by `licenseKeyId`; naming only touches `systemName`. Portal `schedule-copy`/service tests operate purely on License Key IDs. |
| e | Create a daily Schedule via the Portal picker; Kiosk fetches and applies it | PASS | Opt-in integration spec: daily schedule with 2 blocks created via the service; key-only GET returns it; captured live payload parsed and evaluated by the Kiosk (`SchedulePortalParityTest`). |
| f | Weekly: Block boundaries, adjacent (end 7:00 / start 7:00), midnight-crossing, Sunday→Monday spillover | PASS + AUTO | Integration spec: weekly sun crossing + adjacent mon blocks, Sunday spillover collision rejected. Engine tests: adjacency boundary, midnight self-wrap, Sunday→Monday spillover. |
| g | Gap plays local Proclamations; `dynamicAudio` true → dynamic, false → local | AUTO | Engine tests: gaps return no block; per-block `dynamicAudio` flag; `ScrollerPage::effectiveDynamicAudio` honored via playback wiring (build passes). |
| h | Schedule toggle Off stops schedule effects; On resumes | AUTO | `ScheduleManagerTest.DisabledReturnsNoBlock`; toggle stored in `config.yaml` (`schedule_enabled`, default on). |
| i | Schedules assigned to system A never affect system B | PASS | Integration spec: A daily, B weekly simultaneously; independent payloads. |
| j | Copy a Schedule to another named system; that Kiosk picks it up | PASS | Integration spec: copy A→second system yields daily with 2 blocks; independence confirmed; target ETag differs. Portal `schedule-copy.spec` covers overwrite/multi-target/rollback/new IDs. |
| k | Offline uses cached Schedule; corrupt cache ignored; timezone change re-evaluates | AUTO | `ScheduleCacheTest` (round-trip, corrupt ignored), `ScheduleManagerTest` (offline cache, 304 handled). Timezone is passed as wall-clock minutes (wall-clock means DST-safe). |
| l | Portal shows geoip/fallback timezone with kiosk-timezone note; manual minute entry alongside 5-minute options | AUTO | UI note is browser-fallback (no external dep); `TimeInput` tests cover 5-minute options and typed minutes. Visual render MANUAL. |
## Additional checks
| Check | Result | Notes |
| --------------------------------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Old-Kiosk compatibility (validate without `systemName`; legacy schedule endpoint) | PASS | `200 unchanged`; name never cleared; legacy `GET /api/v2/schedule` still returns `email` + `blocks`; modern vs legacy `version` equal. |
| Cross-account isolation | PASS | Legacy schedule with another account's key → `400`; copy rejects non-owned source/targets. |
| Conditional GET | PASS | Matching `If-None-Match` → `304`; stale ETag → `200`; unknown key → `404`. |
| Migration up | PASS | Applied cleanly to a fresh DB (29 migrations); fresh seed succeeded. |
| Migration down/up on realistic data | PASS (with caveat) | Phase 2 and Phase 3 down/up round-tripped structurally on a clone; **the Phase 3 down path is lossy** (deletes per-License-Key schedules). Documented in the contract (Open item 22) and release notes. |
| Portal `svelte-check` / ESLint / tests | PASS | 0 errors/warnings; 394 passed, 4 skipped (opt-in integration). |
| Kiosk build + tests | PASS | Full build; 21 test binaries pass. |
| Formatters | PASS | Portal Prettier clean except the pre-existing `.gitea/workflows/deploy.yaml` (unchanged from before this branch). Kiosk follows `.clang-format` (no binary available to run). |
## Not verified here (requires hardware/display)
The interactive SDL Kiosk UI (rendering, key handling, onboarding walkthrough)
was not driven; all Kiosk behavior above is covered by the engine/client/cache
tests and by parsing live Portal payloads. See the manual checklist in
`scheduler-overhaul-release-notes.md`.
+164
View File
@@ -0,0 +1,164 @@
# Scheduler Overhaul — Release Notes
Status: prepared for rollout. These are **instructions only**; nothing here has
been executed. The contract lives at `docs/schedule-api-contract.md` (kept
byte-identical in both repos).
This document is kept **byte-identical** in both repositories:
- `radiantwave` (C++/SDL3 Kiosk app) — `docs/scheduler-overhaul-release-notes.md`
- `portal` (SvelteKit/Node) — `docs/scheduler-overhaul-release-notes.md`
---
## What changed
### Portal
- **Schedules are assigned to a License Key**, not a user. `Schedule` gained
`licenseKeyId` (unique, nullable) and `type` (`daily` | `weekly` | null);
`Block` gained `day` (null for daily, `mon`..`sun` for weekly) and its
`dynamicAudio` default became `true`.
- **New Kiosk endpoint** `GET /api/v2/licenses/{licenseKey}/schedule` returns the
target payload (`version`, `type`, `daily`, `weekly`, `blocks`) with an `ETag`
derived from `version`, supports conditional GET (`304`), and is `404` for an
unknown key.
- **License validation** (`POST /api/v2/license`) accepts an optional
`systemName` and returns an additive `systemNameStatus`
(`ok` | `unchanged` | `conflict` | `invalid`). Validation always succeeds; a
non-`ok` name status is advisory.
- **New Kiosk endpoint** `PUT /api/v2/licenses/{licenseKey}/system-name`
(`200` / `400 invalid_name` / `404 unknown_license` / `409 name_taken`,
idempotent).
- **System Name** is a human-readable label for a License Key. It is set only
by the Kiosk and shown read-only in the Portal (dashboard and admin lists),
falling back to displaying the License Key. There is no Portal rename path.
Names are unique per account, compared case-insensitively, trimmed, 1–40
characters.
- **Schedule editor** (protected user route `(protected)/(user)/schedule`):
a per-system picker labelled by System Name, a Daily/Weekly type selector with
confirmation when switching a populated schedule (daily→weekly prefills all
seven days; weekly→daily and clearing start empty), per-day weekly editing,
AM/PM `TimeInput` (5-minute suggestions plus manual minute entry), midnight
crossing labelled "ends next day", an on-by-default dynamic-audio toggle, gap
helper text, and an informational note that times run in the Kiosk's own
timezone (browser timezone fallback; no external GeoIP service).
- **Copy to…** copies one system's Schedule to one or more other systems in the
same account, by value, in a single database transaction (all or none). Target
Blocks get fresh IDs so target `version`/ETag changes; `dynamicAudio` and
Proclamation order are preserved. Overwriting a target that already has a
schedule requires explicit confirmation. Copies are independent.
- **OpenAPI** (`docs/openapi.yaml`) documents the new endpoints and payloads.
### Kiosk (`radiantwave`)
- **12-hour AM/PM** time display everywhere the Kiosk shows a time; storage and
parsing remain 24-hour `HH:MM`. The Date Format setting is unchanged.
- **System Settings** menu contains Email Address, License Key, **System Name**,
**Schedule** (on/off), WiFi Setup, Time Zone Setup, and Date Format. The list
is data-driven.
- **System Name** entry (trimmed, 1–40), sent on license validation and via
`PUT /api/v2/licenses/{key}/system-name`. Conflicts/invalid names are reverted
locally with a user-facing message; network failures keep the new name locally
and retry (pending-sync flag) on boot, on validation, and on a timer.
- **Schedule client**: conditional GET (`If-None-Match`) of
`GET /api/v2/licenses/{key}/schedule`, on boot, on network-up, and every
5 minutes. The last payload is cached to
`~/.local/radiantwave/schedule_cache.json` with an **atomic write** (temp file
- rename); a corrupt cache is ignored. Offline uses the cache; no cache and no
network behaves as schedules-off.
- **Schedule engine**: given the payload, the local wall-clock time in the
Kiosk's Timezone setting, and the weekday, picks the active Block. Half-open
`[start, end)` boundaries; midnight-crossing Blocks belong to their start day
(weekly Sunday spills into Monday); invalid Blocks are skipped with a warning
and treated as gaps. Active Block → its Proclamations; `dynamicAudio` true
forces dynamic audio, false uses the locally chosen audio; gaps / no schedule
/ toggle off use local selection exactly as before.
- **Schedule toggle** in System Settings (`proclamation.schedule_enabled`,
default on, Kiosk-local, not synced). Off takes no action but the cache keeps
updating.
---
## Migration notes
- **Portal migrations are additive `up` migrations** and are safe to deploy
while old Kiosks keep running:
- `20260929120000_add_license_key_system_name` — adds
`LicenseKey.systemName` / `systemNameLower` and a unique index on
`(userId, systemNameLower)`.
- `20260929130000_move_schedule_to_license_key` — adds `Schedule.licenseKeyId`
/ `type` and `Block.day`, changes the `dynamicAudio` default to `true`,
makes the legacy `Schedule.userId` optional/non-unique, and backfills each
existing per-user Schedule to that account's **oldest** License Key as a
**daily** schedule. Schedules that cannot be mapped (no owning user/license)
are retained with `licenseKeyId NULL` and reported via a migration `NOTICE`
— they are never silently dropped.
- **Existing (per-user) schedules survive the `up` migration** and are mapped to
a License Key on the same account.
- **⚠ The `down` path is lossy for data created under the new model.** Back up
`Schedule`, `Block`, and `BlockProclamations` before any down-migration.
Rolling back past the Phase 3 migration deletes per-License-Key schedules (see
the contract's Open item 22).
- No Portal schema change affects Kiosks (the Kiosk stores nothing new on the
server beyond the System Name label and its schedule mapping).
---
## Recommended rollout order
Each step is independent; do not skip the verification gates. Do **not** tag,
push, or deploy as part of reading this document — these are instructions.
### Step 1 — Run Portal migrations and deploy the Portal
1. Back up the Portal database (at minimum the `Schedule`, `Block`,
`BlockProclamations`, and `LicenseKey` tables).
2. Apply migrations: `npm run deploy` (which runs `npx prisma migrate deploy`).
3. Confirm `npx prisma migrate status` reports the database is up to date and
review the migration logs for the unmappable-schedule `NOTICE`, if any.
4. Deploy the Portal build (the existing CI/deploy path).
**Rollback (Step 1):** redeploy the previous Portal build. Because the `up`
migrations are additive, the old Portal continues to work against the migrated
schema; no schema rollback is required to redeploy the old Portal. If a schema
rollback is unavoidable, restore from the Step-1 backup (the `down` path is
lossy — see Migration notes).
### Step 2 — Verify old Kiosks still work
1. Against the deployed Portal, confirm an **old Kiosk** (no `systemName`, using
the legacy endpoints) still:
- validates via `POST /api/v2/license` (no `systemName`) and receives `200`;
- reads `GET /api/v2/schedule?email=…&licenseKey=…` and still finds its
`blocks`;
- is unaffected by the new fields, and its stored System Name (if any) is
never cleared.
2. Confirm the Portal dashboard/admin show System Names read-only.
**Rollback (Step 2):** none needed — this step is read-only verification. If old
Kiosks misbehave, roll back the Portal (Step 1 rollback) before proceeding.
### Step 3 — Release the Kiosk build
1. Build and validate the Kiosk through the normal path: tag → CI → the
auto-update channel. (Tagging triggers production CI; do it only when ready.)
2. Confirm the released build reports the expected version and updates in place.
3. Spot-check a device after update: 12-hour times, System Settings contents,
schedule on/off, and that an assigned Schedule is fetched and applied.
**Rollback (Step 3):** use the existing auto-update rollback (re-publish or
pin the previous known-good Kiosk version). The Kiosk tolerates both old and new
Portal payloads, so a Portal rollback alone does not require a Kiosk rollback,
and vice versa.
---
## Known limitations
- Kiosk API endpoints are **unauthenticated** (key-only), mitigated by a
10 requests/min/IP rate limit. Authentication is out of scope for this
overhaul.
- The Phase 3 migration's `down` path is lossy for new-model schedules
(see Migration notes and contract Open item 22).
- The Portal does not offer a rename path for System Names by design.
+166
View File
@@ -0,0 +1,166 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light dark" />
<title>Verify your email address</title>
</head>
<body
style="
margin: 0;
padding: 0;
background-color: #f1f5f9;
-webkit-font-smoothing: antialiased;
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
"
>
<!-- Preheader (hidden) -->
<div style="display: none; max-height: 0; overflow: hidden; opacity: 0; color: transparent">
Confirm your email address to activate your Radiant Wave account.
</div>
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
style="background-color: #f1f5f9; padding: 32px 12px"
>
<tr>
<td align="center">
<table
role="presentation"
width="600"
cellpadding="0"
cellspacing="0"
style="
width: 100%;
max-width: 600px;
background-color: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 16px;
overflow: hidden;
"
>
<tr>
<td style="padding: 28px 32px 0 32px">
<span
style="
display: inline-block;
background-color: #4f46e5;
border-radius: 10px;
padding: 10px 14px;
color: #ffffff;
font-size: 16px;
font-weight: 700;
letter-spacing: -0.01em;
"
>
Radiant Wave
</span>
</td>
</tr>
<tr>
<td style="padding: 24px 32px 0 32px">
<h1
style="
margin: 0 0 12px 0;
color: #0f172a;
font-size: 24px;
line-height: 1.25;
font-weight: 700;
"
>
Confirm your email address
</h1>
<p style="margin: 0 0 8px 0; color: #334155; font-size: 16px; line-height: 1.6">
Thanks for signing up for Radiant Wave. Verify your email address to activate your
account.
</p>
</td>
</tr>
<tr>
<td style="padding: 16px 32px 0 32px">
<table role="presentation" cellpadding="0" cellspacing="0">
<tr>
<td style="border-radius: 10px; background-color: #4f46e5">
<a
href="{{{verification_link}}}"
target="_blank"
style="
display: inline-block;
padding: 14px 28px;
color: #ffffff;
font-size: 16px;
font-weight: 600;
text-decoration: none;
border-radius: 10px;
"
>
Verify Email Address
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding: 16px 32px 0 32px">
<p style="margin: 0; color: #64748b; font-size: 13px; line-height: 1.6">
If the button doesn't work, copy and paste this link into your browser:
</p>
<p
style="
margin: 6px 0 0 0;
word-break: break-all;
font-size: 13px;
line-height: 1.6;
"
>
<a
href="{{{verification_link}}}"
target="_blank"
style="color: #4f46e5; text-decoration: underline"
>{{{verification_link}}}</a
>
</p>
</td>
</tr>
<tr>
<td style="padding: 24px 32px 32px 32px">
<hr style="border: none; border-top: 1px solid #e2e8f0; margin: 0 0 16px 0" />
<p style="margin: 0; color: #94a3b8; font-size: 12px; line-height: 1.6">
This link expires in 1 hour. If you didn't create an account, you can safely
ignore this email.
</p>
</td>
</tr>
</table>
<table
role="presentation"
width="600"
cellpadding="0"
cellspacing="0"
style="width: 100%; max-width: 600px"
>
<tr>
<td style="padding: 20px 8px; text-align: center">
<p style="margin: 0; color: #94a3b8; font-size: 12px; line-height: 1.6">
Sent to {{{target_email_address}}}<br />
&copy; Radiant Wave Technologies
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
+184
View File
@@ -0,0 +1,184 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light dark" />
<title>Your Radiant Wave License Key</title>
</head>
<body
style="
margin: 0;
padding: 0;
background-color: #f1f5f9;
-webkit-font-smoothing: antialiased;
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
"
>
<!-- Preheader (hidden) -->
<div style="display: none; max-height: 0; overflow: hidden; opacity: 0; color: transparent">
Here's your Radiant Wave license key.
</div>
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
style="background-color: #f1f5f9; padding: 32px 12px"
>
<tr>
<td align="center">
<table
role="presentation"
width="600"
cellpadding="0"
cellspacing="0"
style="
width: 100%;
max-width: 600px;
background-color: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 16px;
overflow: hidden;
"
>
<tr>
<td style="padding: 28px 32px 0 32px">
<span
style="
display: inline-block;
background-color: #4f46e5;
border-radius: 10px;
padding: 10px 14px;
color: #ffffff;
font-size: 16px;
font-weight: 700;
letter-spacing: -0.01em;
"
>
Radiant Wave
</span>
</td>
</tr>
<tr>
<td style="padding: 24px 32px 0 32px">
<h1
style="
margin: 0 0 12px 0;
color: #0f172a;
font-size: 24px;
line-height: 1.25;
font-weight: 700;
"
>
Your license key
</h1>
<p style="margin: 0 0 8px 0; color: #334155; font-size: 16px; line-height: 1.6">
Hi {{{firstName}}}, your Radiant Wave account is ready. Here's your license key —
you'll need it to activate the app.
</p>
</td>
</tr>
<tr>
<td style="padding: 20px 32px 0 32px">
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
style="background-color: #f8fafc; border: 1px dashed #cbd5e1; border-radius: 12px"
>
<tr>
<td style="padding: 18px 20px; text-align: center">
<p
style="
margin: 0 0 6px 0;
color: #64748b;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
"
>
Your License Key
</p>
<p
style="
margin: 0;
color: #0f172a;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 20px;
font-weight: 700;
letter-spacing: 0.06em;
word-break: break-all;
"
>
{{{license_key}}}
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding: 20px 32px 0 32px">
<table role="presentation" cellpadding="0" cellspacing="0">
<tr>
<td style="border-radius: 10px; background-color: #4f46e5">
<a
href="https://portal.radiantwavetech.com/dashboard"
target="_blank"
style="
display: inline-block;
padding: 14px 28px;
color: #ffffff;
font-size: 16px;
font-weight: 600;
text-decoration: none;
border-radius: 10px;
"
>
Go to Your Dashboard
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding: 24px 32px 32px 32px">
<hr style="border: none; border-top: 1px solid #e2e8f0; margin: 0 0 16px 0" />
<p style="margin: 0; color: #94a3b8; font-size: 12px; line-height: 1.6">
Keep this key safe — anyone who has it can activate the app. If you didn't request
this, contact support.
</p>
</td>
</tr>
</table>
<table
role="presentation"
width="600"
cellpadding="0"
cellspacing="0"
style="width: 100%; max-width: 600px"
>
<tr>
<td style="padding: 20px 8px; text-align: center">
<p style="margin: 0; color: #94a3b8; font-size: 12px; line-height: 1.6">
&copy; Radiant Wave Technologies
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
+166
View File
@@ -0,0 +1,166 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light dark" />
<title>Reset your Radiant Wave password</title>
</head>
<body
style="
margin: 0;
padding: 0;
background-color: #f1f5f9;
-webkit-font-smoothing: antialiased;
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
"
>
<!-- Preheader (hidden) -->
<div style="display: none; max-height: 0; overflow: hidden; opacity: 0; color: transparent">
Choose a new password for your Radiant Wave account.
</div>
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
style="background-color: #f1f5f9; padding: 32px 12px"
>
<tr>
<td align="center">
<table
role="presentation"
width="600"
cellpadding="0"
cellspacing="0"
style="
width: 100%;
max-width: 600px;
background-color: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 16px;
overflow: hidden;
"
>
<tr>
<td style="padding: 28px 32px 0 32px">
<span
style="
display: inline-block;
background-color: #4f46e5;
border-radius: 10px;
padding: 10px 14px;
color: #ffffff;
font-size: 16px;
font-weight: 700;
letter-spacing: -0.01em;
"
>
Radiant Wave
</span>
</td>
</tr>
<tr>
<td style="padding: 24px 32px 0 32px">
<h1
style="
margin: 0 0 12px 0;
color: #0f172a;
font-size: 24px;
line-height: 1.25;
font-weight: 700;
"
>
Reset your password
</h1>
<p style="margin: 0 0 8px 0; color: #334155; font-size: 16px; line-height: 1.6">
We received a request to reset your Radiant Wave password. Click the button below
to choose a new one.
</p>
</td>
</tr>
<tr>
<td style="padding: 16px 32px 0 32px">
<table role="presentation" cellpadding="0" cellspacing="0">
<tr>
<td style="border-radius: 10px; background-color: #4f46e5">
<a
href="{{{reset_link}}}"
target="_blank"
style="
display: inline-block;
padding: 14px 28px;
color: #ffffff;
font-size: 16px;
font-weight: 600;
text-decoration: none;
border-radius: 10px;
"
>
Reset Password
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding: 16px 32px 0 32px">
<p style="margin: 0; color: #64748b; font-size: 13px; line-height: 1.6">
If the button doesn't work, copy and paste this link into your browser:
</p>
<p
style="
margin: 6px 0 0 0;
word-break: break-all;
font-size: 13px;
line-height: 1.6;
"
>
<a
href="{{{reset_link}}}"
target="_blank"
style="color: #4f46e5; text-decoration: underline"
>{{{reset_link}}}</a
>
</p>
</td>
</tr>
<tr>
<td style="padding: 24px 32px 32px 32px">
<hr style="border: none; border-top: 1px solid #e2e8f0; margin: 0 0 16px 0" />
<p style="margin: 0; color: #94a3b8; font-size: 12px; line-height: 1.6">
This link expires in 1 hour. If you didn't request a password reset, you can
safely ignore this email — your password won't change.
</p>
</td>
</tr>
</table>
<table
role="presentation"
width="600"
cellpadding="0"
cellspacing="0"
style="width: 100%; max-width: 600px"
>
<tr>
<td style="padding: 20px 8px; text-align: center">
<p style="margin: 0; color: #94a3b8; font-size: 12px; line-height: 1.6">
Sent to {{{target_email_address}}}<br />
&copy; Radiant Wave Technologies
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
+174
View File
@@ -0,0 +1,174 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light dark" />
<title>Welcome to Radiant Wave</title>
</head>
<body
style="
margin: 0;
padding: 0;
background-color: #f1f5f9;
-webkit-font-smoothing: antialiased;
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
"
>
<!-- Preheader (hidden) -->
<div style="display: none; max-height: 0; overflow: hidden; opacity: 0; color: transparent">
Set your password to finish setting up your Radiant Wave account.
</div>
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
style="background-color: #f1f5f9; padding: 32px 12px"
>
<tr>
<td align="center">
<table
role="presentation"
width="600"
cellpadding="0"
cellspacing="0"
style="
width: 100%;
max-width: 600px;
background-color: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 16px;
overflow: hidden;
"
>
<tr>
<td style="padding: 28px 32px 0 32px">
<span
style="
display: inline-block;
background-color: #4f46e5;
border-radius: 10px;
padding: 10px 14px;
color: #ffffff;
font-size: 16px;
font-weight: 700;
letter-spacing: -0.01em;
"
>
Radiant Wave
</span>
</td>
</tr>
<tr>
<td style="padding: 24px 32px 0 32px">
<h1
style="
margin: 0 0 12px 0;
color: #0f172a;
font-size: 24px;
line-height: 1.25;
font-weight: 700;
"
>
Welcome to Radiant Wave
</h1>
<p style="margin: 0 0 8px 0; color: #334155; font-size: 16px; line-height: 1.6">
An account has been created for you. Set a password to finish setting up your
account and access your dashboard.
</p>
</td>
</tr>
<tr>
<td style="padding: 16px 32px 0 32px">
<table role="presentation" cellpadding="0" cellspacing="0">
<tr>
<td style="border-radius: 10px; background-color: #4f46e5">
<a
href="{{{registration_link}}}"
target="_blank"
style="
display: inline-block;
padding: 14px 28px;
color: #ffffff;
font-size: 16px;
font-weight: 600;
text-decoration: none;
border-radius: 10px;
"
>
Set Your Password
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding: 16px 32px 0 32px">
<p style="margin: 0; color: #64748b; font-size: 13px; line-height: 1.6">
If the button doesn't work, copy and paste this link into your browser:
</p>
<p
style="
margin: 6px 0 0 0;
word-break: break-all;
font-size: 13px;
line-height: 1.6;
"
>
<a
href="{{{registration_link}}}"
target="_blank"
style="color: #4f46e5; text-decoration: underline"
>{{{registration_link}}}</a
>
</p>
</td>
</tr>
<tr>
<td style="padding: 24px 32px 0 32px">
<p style="margin: 0; color: #334155; font-size: 15px; line-height: 1.6">
After you set your password, sign in and complete your profile to get started.
</p>
</td>
</tr>
<tr>
<td style="padding: 24px 32px 32px 32px">
<hr style="border: none; border-top: 1px solid #e2e8f0; margin: 0 0 16px 0" />
<p style="margin: 0; color: #94a3b8; font-size: 12px; line-height: 1.6">
This link expires in 1 hour. If you didn't expect this email, you can safely
ignore it.
</p>
</td>
</tr>
</table>
<table
role="presentation"
width="600"
cellpadding="0"
cellspacing="0"
style="width: 100%; max-width: 600px"
>
<tr>
<td style="padding: 20px 8px; text-align: center">
<p style="margin: 0; color: #94a3b8; font-size: 12px; line-height: 1.6">
Sent to {{{target_email_address}}}<br />
&copy; Radiant Wave Technologies
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
+6
View File
@@ -37,5 +37,11 @@ export default defineConfig(
svelteConfig
}
}
},
{
files: ['**/*.spec.ts', '**/*.spec.js'],
rules: {
'@typescript-eslint/no-explicit-any': 'off'
}
}
);
+2 -2
View File
@@ -1,11 +1,11 @@
{
"name": "portal.Rhemawavetech.com",
"name": "portal.radiantwavetech.com",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "portal.Rhemawavetech.com",
"name": "portal.radiantwavetech.com",
"version": "0.0.1",
"dependencies": {
"@prisma/adapter-pg": "^7.8.0",
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "portal.Rhemawavetech.com",
"name": "portal.radiantwavetech.com",
"private": true,
"version": "0.0.1",
"type": "module",
-756
View File
@@ -1,756 +0,0 @@
# RadiantWave Portal API v2 Schema
This document defines the **required** API surface for the RadiantWave kiosk
application and serves as the implementation contract for the portal
(`../portal`).
It supersedes `payloads.md` and `ApiUpdates.md`, which described a v1 API that
does not meet the application's requirements.
---
## Background and constraints
- The kiosk application (`this repo`) talks to
`https://portal.radiantwavetech.com/api/v1`.
- The **existing v1 routes must not change**: previously shipped kiosk binaries
depend on them. The v1 routes and their exact request/response shapes stay
frozen.
- To bring the API into compliance with this application's expectations we
introduce a **new, versioned API under `/api/v2`**, and migrate the kiosk
client to call `/api/v2` instead of `/api/v1`.
- The kiosk client currently uses a single base URL
(`src/network/api_client.h` → `API_BASE_URL`). All endpoint paths are
relative to that base, so migrating the base URL is the only client-side
change required.
Compliance means: every endpoint the kiosk parses must return exactly the
fields the kiosk expects, in the shapes documented below. Deviations break
provisioning, content sync, or rendering.
---
## Versioning strategy
| Route space | Status | Consumer |
|-------------|---------------------------|-------------------------------------------------------|
| `/api/v1/*` | Frozen, never modified | Old/shipped kiosk binaries, GoHighLevel automation |
| `/api/v2/*` | New (this document) | Current kiosk application (after base-URL migration) |
Rules:
1. Never change the behavior or response shape of an existing `/api/v1/*`
route. If a v1 fix is required, fix it in v2.
2. New capabilities ship under `/api/v2` only.
3. A consumer may pin to the major version it was built against. The kiosk
pins to `/api/v2`.
---
## General conventions (v2)
- Base URL: `https://portal.radiantwavetech.com/api/v2`.
- All routes require HTTPS (TLS 1.2+; TLS 1.3 recommended).
- Success responses are JSON with `content-type: application/json`.
- Error bodies use the shape `{"message": "..."}` for route-handler errors and
`{"error": "..."}` for the authentication layer (see Security).
- Any response code outside the documented success code is a failure; the
kiosk surfaces `message`/`error` text when present.
- API keys are sent as `Authorization: Bearer sk_<hex>` and are **required on
every v2 route** except the signed onboarding route (below).
- Credentials (license keys, emails) must **never** be passed in the query
string. v1 accepted `?email=&key=`; v2 must not.
- Request body sizes are capped (see Security); oversized bodies are rejected
with `413`.
---
## Canonical data model: solfeggio frequencies
The kiosk uses a fixed 9-preset frequency scheme. Frequency ids must be
**canonical** — they appear on proclamations and color palettes and must match
the kiosk's own `frequencyIdForHz()` mapping:
| Canonical id | Hz | Frequency (kiosk label) |
|--------------|------|------------------------------|
| 1 | 174 | Grounding / Relief (285 Hz is an alias of id 1) |
| 2 | 396 | Root |
| 3 | 417 | Sacral |
| 4 | 432 | Clarity / Wellbeing |
| 5 | 528 | Solar Plexus (fallback palette) |
| 6 | 639 | Heart |
| 7 | 741 | Throat |
| 8 | 852 | 3rd Eye |
| 9 | 963 | Crown |
**Compliance gap in v1:** the portal's `SolfeggioFrequency` table stores 7
rows whose ids do **not** match this scheme (id 1 = 936 Hz, id 5 = 528 Hz,
id 7 = 396 Hz, etc.). Because proclamations and color palettes are keyed by
frequency id, returning portal ids would make the kiosk look up the wrong
palette (e.g. a 396 Hz proclamation would render with the 741 Hz palette).
**v2 requirement:** every `solfeggioFrequencyId` emitted by v2 must be a
canonical id (1–9) whose `value` resolves through the kiosk map above.
Frequencies that have no canonical id (e.g. 936 Hz) must not be referenced by
any v2 proclamation or palette. See Implementation notes for the reconciling
strategy.
---
## Endpoints
### 1. `GET /api/v2/frequencies`
Lists the canonical solfeggio frequencies.
Request:
```
GET /api/v2/frequencies
Authorization: Bearer sk_<hex>
```
Response (200):
```json
[
{ "id": 1, "name": "Grounding / Relief", "value": 174 },
{ "id": 2, "name": "Root", "value": 396 },
{ "id": 3, "name": "Sacral", "value": 417 },
{ "id": 4, "name": "Clarity / Wellbeing", "value": 432 },
{ "id": 5, "name": "Solar Plexus", "value": 528 },
{ "id": 6, "name": "Heart", "value": 639 },
{ "id": 7, "name": "Throat", "value": 741 },
{ "id": 8, "name": "3rd Eye", "value": 852 },
{ "id": 9, "name": "Crown", "value": 963 }
]
```
Contract (kiosk `parseFrequenciesJson`):
- `id`: int, 1–9, **non-zero** (entries with id 0 are skipped).
- `name`: string.
- `value`: int, Hz. Should be a value the kiosk's `frequencyIdForHz()` map
recognizes (174, 285, 396, 417, 432, 528, 639, 741, 852, 963).
Errors:
| Code | Body | Meaning |
|------|------|---------|
| 200 | array of frequency objects | Success |
| 401 | `{"error": "..."}` | Missing/invalid API key |
---
### 2. `GET /api/v2/categories`
Lists all proclamation categories (identical shape to v1).
Request:
```
GET /api/v2/categories
Authorization: Bearer sk_<hex>
```
Response (200):
```json
[
{ "id": 4, "name": "Spiritual", "sortOrder": 0, "parentId": null },
{ "id": 5, "name": "General", "sortOrder": 0, "parentId": 4 }
]
```
Contract (kiosk `parseCategoriesJson`):
- `id`: int, **non-zero** (id 0 entries are skipped).
- `name`: string.
- `parentId`: int for children, **`null` for root** categories (kiosk maps
null → 0).
- `sortOrder`: int.
Errors:
| Code | Body | Meaning |
|------|------|---------|
| 200 | array of category objects | Success |
| 401 | `{"error": "..."}` | Missing/invalid API key |
---
### 3. `GET /api/v2/proclamations`
Returns the proclamation library. This is the kiosk's primary content sync.
Request (default: full library):
```
GET /api/v2/proclamations
Authorization: Bearer sk_<hex>
```
Optional entitlement filter (full records, see below):
```
GET /api/v2/proclamations
Authorization: Bearer sk_<hex>
X-License-Key: XXXX-XXXX-XXXX-XXXX
X-Email: user@example.com
```
> Header-based credentials replace the v1 `?email=&key=` query form. Do not
> read credentials from the URL.
Response (200) — every item is a **full proclamation record**:
```json
[
{
"id": "b40fdcb9-703c-420e-af32-a081964c6f08",
"name": "Peace & Relaxation",
"content": "<p>The text content, HTML formatted</p>",
"categoryId": 5,
"solfeggioFrequencyId": 5,
"subscriptionLevelId": 1,
"colorPalette": ["#32CD32", "#3CB371", "#90EE90", "#98FB98", "#ADFFA9", "#C8FFD2", "#F0FFF0"]
}
]
```
Contract (kiosk `parseProclamationsJson` + `parseProclamationPalettesJson`):
- `id`: string UUID, **required non-empty** (empty ids are skipped).
- `name`: string, used as the display title. Kiosk strips HTML from it.
- `content`: string, **HTML** (the kiosk strips tags and unescapes entities
before rendering). Plain text is also acceptable but HTML is the convention.
- `categoryId`: int, maps to a `/categories` id.
- `solfeggioFrequencyId`: int, **must be a canonical id (1–9)** — see the
canonical model above.
- `subscriptionLevelId`: int or `null`. The kiosk treats missing/null/≤0 as
level 1 (Basic).
- `colorPalette` (optional): array of exactly 7 `"#RRGGBB"` strings, indexed
color 0 (primary) through color 6 (tertiary). When present, the kiosk can
color a proclamation directly from this payload (first proclamation per
frequency wins for palette derivation). When absent, the kiosk falls back to
`/color-palettes`, then to its own 528 Hz seed.
With entitlement headers (`X-License-Key` + `X-Email`):
- Validate the key exists, is active, and belongs to the email's user.
- Return the **full proclamation records** the user may access (matching the
record shape above), **not** raw `UserProclamations` join-table rows.
(v1 returned join rows, which the kiosk parser cannot consume — this is the
primary v1 compliance defect this endpoint fixes.)
Errors:
| Code | Body | Meaning |
|------|------|---------|
| 200 | array of proclamation records | Success |
| 400 | `{"message": "not-authorized"}` | Entitlement headers supplied but key or email does not exist |
| 400 | `{"message": "this key doesn't appear to belong to you"}` | Key exists but owned by a different user |
| 401 | `{"error": "..."}` | Missing/invalid API key |
---
### 4. `POST /api/v2/license`
Validates a license key / email pair and returns the full subscription level.
Request:
```
POST /api/v2/license
Content-Type: application/json
Authorization: Bearer sk_<hex>
```
```json
{
"license_key": "XXXX-XXXX-XXXX-XXXX",
"email_address": "user@example.com"
}
```
Response (200):
```json
{
"key": "XXXX-XXXX-XXXX-XXXX",
"email_address": "user@example.com",
"subscription_level": {
"id": 2,
"name": "Advanced",
"description": "General + Specific"
}
}
```
`subscription_level` is **always an object** `{ id, name, description }`. If
the license's level name does not match any `SubscriptionLevel` row, fall back
to a structurally consistent object:
```json
{
"key": "XXXX-XXXX-XXXX-XXXX",
"email_address": "user@example.com",
"subscription_level": { "id": null, "name": "SomeCustomLevel", "description": null }
}
```
Contract (kiosk `parseLicenseJson`):
- `key`: string, **required non-empty**.
- `email_address`: string.
- `subscription_level.name`: string, **required** for a successful validation
(the kiosk persists id/name/description into its local `system_kv` table and
uses the level for proclamation gating and the Home footer).
Errors:
| Code | Body | Meaning |
|------|------|---------|
| 200 | success object | License valid and owned by the email |
| 400 | `{"message": "license_key is required"}` | Missing `license_key` |
| 400 | `{"message": "email_address is required"}` | Missing `email_address` |
| 400 | `{"message": "license key not found"}` | No key with that value |
| 400 | `{"message": "license key is not active"}` | Key exists but inactive |
| 400 | `{"message": "user does not exist"}` | No user with that email |
| 400 | `{"message": "key does not belong to specified user"}` | Key owned by a different user |
| 401 | `{"error": "..."}` | Missing/invalid API key |
> The kiosk client also tolerates the v1 string form and flat
> `subscription_level_id` / `subscription_level_description` fields. v2 should
> always send the object form; the fallbacks exist only for transitional
> compatibility.
---
### 5. `GET /api/v2/subscription-levels`
Lists the subscription levels.
Request:
```
GET /api/v2/subscription-levels
Authorization: Bearer sk_<hex>
```
Response (200):
```json
[
{ "id": 1, "name": "Basic", "description": "General only" },
{ "id": 2, "name": "Advanced", "description": "General + Specific" },
{ "id": 3, "name": "Elite", "description": "General + Specific + Precise" }
]
```
Errors:
| Code | Body | Meaning |
|------|------|---------|
| 200 | array of `{id, name, description}` | Success |
| 401 | `{"error": "..."}` | Missing/invalid API key |
---
### 6. `GET /api/v2/color-palettes`
Exposes the 7-color palette for every canonical frequency so the portal admin
can update colors without a kiosk code release.
Request:
```
GET /api/v2/color-palettes
Authorization: Bearer sk_<hex>
```
Response (200) — one entry per canonical frequency (1–9):
```json
[
{
"solfeggioFrequencyId": 1,
"colors": ["#8B6914", "#B8860B", "#DAA520", "#E8B64C", "#FFD700", "#FFE9A8", "#FFF4CC"]
},
{
"solfeggioFrequencyId": 5,
"colors": ["#32CD32", "#3CB371", "#90EE90", "#98FB98", "#ADFFA9", "#C8FFD2", "#F0FFF0"]
}
]
```
Contract (kiosk `parseColorPalettesJson`):
- `solfeggioFrequencyId`: int, **non-zero**, canonical id.
- `colors`: array of `"#RRGGBB"` (or `"RRGGBB"`) strings, exactly 7.
Kiosk fallback: `db::getPaletteForFrequencyId()` falls back to the 528 Hz
palette for any frequency id with no rows; the startup seed backfills missing
ids with `INSERT OR IGNORE`, so portal values win after a successful sync.
Errors:
| Code | Body | Meaning |
|------|------|---------|
| 200 | array of `{solfeggioFrequencyId, colors}` | Success |
| 401 | `{"error": "..."}` | Missing/invalid API key |
---
### 7. `POST /api/v2/automations/create/account`
Creates (or reuses) a user account and issues a brand-new active license key.
The key is **not returned** — it is emailed to the user. This is the only v2
route that does not require a bearer API key; instead it requires an
**HMAC-SHA256 signature** (see Security). It is used for first-run provisioning
and by the GoHighLevel automation.
Request:
```
POST /api/v2/automations/create/account
Content-Type: application/json
X-Timestamp: <unix seconds>
X-Nonce: <random uuid>
X-Signature: <hex HMAC-SHA256 of canonical request with the onboarding secret>
```
```json
{
"email": "user@example.com",
"name": "Optional Display Name",
"subscription_level": "Basic"
}
```
- `email` (required, string).
- `name` (optional, string; defaults to the local part of `email`).
- `subscription_level` (required, string) — one of `Basic`, `Advanced`,
`Elite` (must match a `SubscriptionLevel.name`).
Response (200):
```json
{
"success": true,
"message": "Account and license key created",
"user_id": "uuid"
}
```
Errors:
| Code | Body | Meaning |
|------|------|---------|
| 200 | success object | Account (+ license key) created |
| 400 | `{"message": "email is required"}` | Missing/empty `email` |
| 400 | `{"message": "subscription_level is required"}` | Missing/empty `subscription_level` |
| 400 | `{"message": "subscription_level \"<name>\" not found"}` | Unknown level name |
| 401 | `{"error": "invalid signature"}` | Bad/expired/replayed HMAC signature |
| 429 | `{"message": "rate limit exceeded"}` | Too many requests from this IP/email |
| 500 | `{"message": "Failed to create account"}` | User creation failed |
| 500 | `{"message": "Failed to find or create user"}` | User could not be located |
Behavior (unchanged from v1):
- If the email already belongs to a user, no new user is created — only a new
license key for that user.
- A new user receives a temporary-password/verify email and a license-key
email. Sending up to two emails, so this endpoint must be rate-limited
aggressively.
---
## Compliance contract (kiosk → v2)
| Kiosk consumer | v2 endpoint | Required fields / shape |
|----------------|-------------|-------------------------|
| `sync::syncProclamationsFromApi` (Home + ProclamationOptions sync) | `GET /proclamations`, `GET /categories` | Full proclamation records; `id`, `name`, `content`, `categoryId`, `solfeggioFrequencyId` (canonical), `subscriptionLevelId` |
| `license::validateStoredLicense` (License Key provisioning page) | `POST /license` | `subscription_level` as `{id, name, description}` object |
| `api::fetchColorPalettes` / `sync::syncColorPalettesFromApi` (readiness, to be wired) | `GET /color-palettes` | `[{solfeggioFrequencyId, colors[7]}]` |
| `sync::syncProclamationPalettesFromApi` (readiness, to be wired) | `GET /proclamations` | Embedded `colorPalette` array per proclamation |
| `api::fetchFrequencies` (readiness) | `GET /frequencies` | Canonical id 1–9 + `value` in Hz |
| Future level-picker UI | `GET /subscription-levels` | `{id, name, description}` |
| Provisioning / GHL automation | `POST /automations/create/account` | `email`, `subscription_level`, optional `name` |
---
## Implementation instructions (portal, `../portal`)
### Route layout
Create the v2 route tree alongside the frozen v1 tree:
```
src/routes/api/v2/
frequencies/+server.ts
categories/+server.ts
proclamations/+server.ts
license/+server.ts
subscription-levels/+server.ts
color-palettes/+server.ts
automations/create/account/+server.ts
```
### Auth gate (`src/hooks.server.ts`)
Extend the existing handler so v2 auth is **always enforced** (do not honor
`API_KEYS_ACTIVE` for v2):
```ts
const isV2 = event.url.pathname.startsWith('/api/v2/');
const isSignedOnboarding =
event.url.pathname.startsWith('/api/v2/automations/');
if (isV2 && !isSignedOnboarding) {
// validateApiKey() ALWAYS runs for v2 — no env-var bypass.
const authHeader = event.request.headers.get('authorization');
if (!authHeader?.startsWith('Bearer ')) {
return new Response(JSON.stringify({ error: 'API key required' }), {
status: 401,
headers: { 'content-type': 'application/json' }
});
}
const record = await validateApiKey(authHeader.slice(7));
if (!record) {
return new Response(JSON.stringify({ error: 'Invalid or expired API key' }), {
status: 401,
headers: { 'content-type': 'application/json' }
});
}
return resolve(event);
}
```
Keep the existing v1 branch untouched.
### Shared serializer (`src/lib/server/v2-serializers.ts`)
Add one module that maps DB rows to the v2 wire format. The critical piece is
frequency-id reconciliation:
```ts
// Canonical id <- Hz (kiosk frequencyIdForHz map).
const CANONICAL_ID_FOR_HZ: Record<number, number> = {
174: 1, 285: 1, 396: 2, 417: 3, 432: 4,
528: 5, 639: 6, 741: 7, 852: 8, 963: 9
};
function canonicalIdForFrequency(freq: { id: number; value: number; name: string }): number | null {
const id = CANONICAL_ID_FOR_HZ[freq.value];
return id ?? null; // e.g. 936 Hz has no canonical id -> excluded from v2
}
export function serializeProclamation(p: Proclamation & { solfeggioFrequency: { value: number } }) {
const solfeggioFrequencyId = CANONICAL_ID_FOR_HZ[p.solfeggioFrequency.value];
if (!solfeggioFrequencyId) return null; // drop proclamations with no canonical frequency
return {
id: p.id,
name: p.name,
content: p.content,
categoryId: p.categoryId,
solfeggioFrequencyId,
subscriptionLevelId: p.subscriptionLevelId,
// optional; read from the palette source below
colorPalette: paletteFor(p.solfeggioFrequency.value) // 7x "#RRGGBB" or omit
};
}
```
Key points:
- Derive the emitted `solfeggioFrequencyId` from the joined frequency's
**Hz value**, never from the DB id. This keeps v1 rows (and their ids)
untouched while v2 emits canonical ids.
- Proclamations whose frequency has no canonical id (e.g. 936 Hz) are excluded
from v2 responses (or re-assigned a canonical frequency in data).
- Add the missing frequencies to the DB so content can be authored for them:
`174, 285, 432, 963` (by `value`). Do not delete or renumber the existing
rows that v1 depends on.
### Color palettes
Add a portal-editable palette source keyed by canonical frequency, for example
a `colorPalette` JSON column on `SolfeggioFrequency` or a
`ColorPalette`/`ColorPaletteColor` table:
```prisma
model ColorPalette {
solfeggioFrequencyId Int @id
colors Json // ["#RRGGBB", ... x7]
}
```
- Seed all 9 canonical palettes to match the kiosk's current seed
(`src/db/database_manager.cpp` → `seedColorPalettes()`).
- `GET /color-palettes` returns one entry per canonical id.
- `GET /proclamations` embeds the palette for the proclamation's canonical
frequency.
### License endpoint
In `license/+server.ts`, resolve the level object like v1 change #1 suggested:
```ts
const level = await prisma.subscriptionLevel.findFirst({
where: { name: storedKey.level }
});
subscription_level = level
? { id: level.id, name: level.name, description: level.description }
: { id: null, name: storedKey.level, description: null };
```
### Proclamations entitlement filter
When `X-License-Key` / `X-Email` headers are present, validate them exactly as
v1 does, but return **full proclamation records** (the user's entitled set via
`UserProclamations` + `Proclamation` include), never join-table rows.
### Tests
- Unit-test the serializers with fixtures matching the kiosk's offline tests
in `tests/test_api_client.cpp` (string vs object `subscription_level`,
`subscriptionLevelId: null`, `parentId: null`, 7-color palettes, canonical
frequency mapping incl. 936 → dropped).
- Live smoke tests against `/api/v2` mirroring the existing v1 live tests.
---
## Security: restricting v2 to the kiosk application
These endpoints should be reachable only from enrolled kiosk devices. v1's
auth could be disabled with `API_KEYS_ACTIVE=false`; v2 must not offer that
bypass. Suggestions below are ordered by impact.
### 1. Mandatory bearer-key auth on every content route
- Every v2 route except the signed onboarding route requires
`Authorization: Bearer sk_<hex>`.
- Enforce it in `hooks.server.ts` unconditionally (no env toggle).
- Keys are generated with `generateApiKey()`, stored **SHA-256 hashed**, and
only the `sk_xxxxxxxxxx` prefix is ever displayed. Never log or return the
plaintext key.
- Use constant-time comparison / lookup by hash (already the case via
`validateApiKey`).
### 2. Per-device credentials (enroll each kiosk)
One shared API key is a single point of failure. Instead:
- Add a `Device` record (id, serial, model, owner, enrolledAt, apiKeyId) and
issue **one unique key per kiosk** at first boot (or at manufacturing).
- Bind the key to the device: include the device id in a custom header
(`X-Device-Id`) and verify the key belongs to that device.
- A compromised key is then revoked per-device without a global rotation, and
the portal can tell which device made each request.
### 3. Least privilege / scoped keys
- Kiosk keys are read-only for content (`/frequencies`, `/categories`,
`/proclamations`, `/color-palettes`, `/subscription-levels`) and may POST to
`/license`.
- Reject any other verb/route combination with 403. Admin and data-mutation
keys are issued separately to portal staff only.
### 4. Rate limiting per key and per IP
- Content GETs: ~60/min per key/IP.
- `/license`: ~10/min per key/IP.
- `/automations/create/account`: ~5/min per IP and ~3/day per email.
- Return `429` with `Retry-After`; the kiosk client should back off and retry.
- Slippery handling for repeated failures (rate-limit on failures too).
### 5. Replay protection / request signing
- The bearer key alone is replayable if captured. Add to the high-risk routes
(and the onboarding route) an HMAC-SHA256 signature over the canonical
request (method, path, `X-Timestamp`, `X-Nonce`, raw body) using a secret
known only to the device and the server.
- Reject when `|now − X-Timestamp| > 300s` or when a nonce has been seen
before. This defeats capture-and-replay attacks over the network.
- Implement the same scheme for the onboarding route using a shared secret
configured on the portal and the GoHighLevel automation (replacing the
current entirely unauthenticated `/automations/*` exemption).
### 6. Never put credentials in URLs
- v1 accepted license keys and emails in the query string, which land in
access logs, proxies, and referrers. v2 reads credentials from the
`Authorization` header and JSON body only.
- Redact `Authorization` values, license keys, and emails in application and
proxy logs.
### 7. Client fingerprinting and bot rejection
- The kiosk sends `User-Agent: RadiantWave-Kiosk/0.1`. Require it (optionally
with a signed `X-Client-Id`) and return `403` for generic browsers, `curl`,
and headless scanners.
- Reject request bodies that are not `application/json` where JSON is
expected; enforce request-size limits (reject with `413`).
- At the proxy layer: require TLS 1.2+, prefer 1.3, set HSTS; consider
restricting v2 to expected deployment geographies and blocking TOR/exit
nodes and known hosting/bot ASNs.
### 8. Input validation and uniform errors
- Validate each body against a strict schema: email ≤ 254 chars and a valid
shape, `license_key` must match `^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$`,
`subscription_level` must equal a known level name, `name` ≤ 120 chars.
- Reject embedded HTML/script in string fields.
- Return uniform `{"message": ...}` bodies; never leak stack traces or SQL
errors. Avoid verbose error differences that enable credential enumeration
(see below).
### 9. License / onboarding abuse resistance
- `/license` success/failure is enumerable by design (a fixed key format). At
minimum rate-limit aggressively. Consider returning a generic
`{"message": "invalid credentials"}` (possibly plus a structured `code`
field for the kiosk) so attackers cannot distinguish "user exists" from
"key exists". Keep the specific messages only behind authenticated admin
tooling if they are needed for support.
- `/automations/create/account` mints real license keys and sends email, so
protect it hardest: HMAC signature (above), per-IP and per-email rate
limits, an email-domain/typo check, and a honeypot field. Never expose it
without signature as an unauthenticated "create account" funnel.
### 10. Monitoring, logging, alerting
- Log every v2 request as structured data: timestamp, endpoint, method, status,
key prefix, `X-Device-Id`, IP, latency. Never log full keys, emails, or
bodies.
- Alert on: error-rate spikes, repeated 401s, unexpected `/license` volume,
signup bursts, single-device request floods, and revoked-key usage.
- Automatically suspend a device that shows abusive patterns; require
re-enrollment.
### 11. Key lifecycle
- Rotate keys on a schedule; support the existing `rotateApiKey` 48-hour grace
and emergency revocation per device.
- Auto-expire keys that have not authenticated in N days.
- Consider an authenticated `/api/v2/auth/refresh` (or device re-enrollment)
so a kiosk can obtain a fresh key, turning a leaked key into a self-healing
problem.
---
## Rollout checklist
1. Implement the v2 routes and serializers; add unit + live smoke tests.
2. Ship v2 alongside v1 (both live; v1 frozen).
3. Flip the kiosk base URL: `src/network/api_client.h` →
`https://portal.radiantwavetech.com/api/v2` (already done by this change).
4. Run the kiosk live tests with `RW_LIVE_API_KEY` set:
- `./build/test_api_client`
- `./build/test_license_service`
5. Update portal-admin color palettes and verify a fresh kiosk sync picks them
up (now wired via `/color-palettes`).
6. Keep `/api/v1` online until all deployed kiosks are on the v2 build;
monitor v1 traffic and sunset it only after the fleet is migrated.
+9
View File
@@ -5,6 +5,15 @@ import { defineConfig } from 'prisma/config';
export default defineConfig({
schema: 'prisma/schema.prisma',
// Legacy subscription tables are retained in the database but are no longer
// managed by Prisma Migrate. They must never be dropped (production may not
// own them and the data is kept for recovery).
experimental: {
externalTables: true
},
tables: {
external: ['public.SubscriptionLevel', 'public.ProclamationCategorySubscriptionLevel']
},
migrations: {
path: 'prisma/migrations',
seed: 'tsx prisma/seed.ts'
@@ -0,0 +1,115 @@
-- Flatten ProclamationCategory hierarchy into a flat list and introduce
-- ProclamationGroup (formerly child sub-categories).
--
-- This migration is intentionally NON-DESTRUCTIVE:
-- * ProclamationGroup / Proclamation.groupId are added additively.
-- * Former child categories are ARCHIVED to ProclamationCategoryLegacy
-- before the hierarchy (parentId) is removed, so no category data is lost.
-- * Subscription-level resources (SubscriptionLevel,
-- ProclamationCategorySubscriptionLevel, Proclamation.subscriptionLevelId,
-- LicenseKey.level) are LEFT IN PLACE as unmanaged legacy data. They are no
-- longer modeled by the Prisma schema and are ignored by the application,
-- but their data is preserved and no DROP is attempted on tables the deploy
-- role may not own.
-- CreateTable
CREATE TABLE "ProclamationGroup" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "ProclamationGroup_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "ProclamationGroup_name_key" ON "ProclamationGroup"("name");
-- Seed groups from every distinct former sub-category name
INSERT INTO "ProclamationGroup" ("name", "sortOrder")
SELECT "name",
ROW_NUMBER() OVER (ORDER BY "name") - 1
FROM (
SELECT DISTINCT "name" FROM "ProclamationCategory" WHERE "parentId" IS NOT NULL
) AS d;
-- AddColumn (nullable during backfill)
ALTER TABLE "Proclamation" ADD COLUMN "groupId" INTEGER;
-- Backfill: former child categories become the proclamation's group; the
-- parent becomes its flat category.
UPDATE "Proclamation" AS p
SET "groupId" = g."id",
"categoryId" = c."parentId"
FROM "ProclamationCategory" AS c
JOIN "ProclamationGroup" AS g ON g."name" = c."name"
WHERE p."categoryId" = c."id" AND c."parentId" IS NOT NULL;
-- Backfill: proclamations that had no group (e.g. 'Custom (Client Specific)')
-- default to the General group, if it exists.
UPDATE "Proclamation"
SET "groupId" = (SELECT "id" FROM "ProclamationGroup" WHERE "name" = 'General')
WHERE "groupId" IS NULL;
-- Safety net: any proclamation still unmapped takes the first group.
UPDATE "Proclamation"
SET "groupId" = (SELECT "id" FROM "ProclamationGroup" ORDER BY "sortOrder" LIMIT 1)
WHERE "groupId" IS NULL;
ALTER TABLE "Proclamation" ALTER COLUMN "groupId" SET NOT NULL;
-- AddForeignKey
ALTER TABLE "Proclamation" ADD CONSTRAINT "Proclamation_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "ProclamationGroup"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- Archive former child categories before removing them, so their data is
-- preserved and recoverable. The legacy table also records each child's
-- parent for audit/recovery.
CREATE TABLE "ProclamationCategoryLegacy" (
"id" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"parentId" INTEGER,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "ProclamationCategoryLegacy_pkey" PRIMARY KEY ("id")
);
INSERT INTO "ProclamationCategoryLegacy" ("id", "name", "parentId", "sortOrder")
SELECT "id", "name", "parentId", "sortOrder"
FROM "ProclamationCategory"
WHERE "parentId" IS NOT NULL;
-- Archive category->subscription mappings for the child categories being
-- removed, so the cascade delete below does not destroy this (deprecated)
-- mapping data. Requires read access to the join table, but never ownership.
CREATE TABLE "ProclamationCategorySubscriptionLevelLegacy" (
"categoryId" INTEGER NOT NULL,
"subscriptionLevelId" INTEGER NOT NULL,
CONSTRAINT "ProclamationCategorySubscriptionLevelLegacy_pkey" PRIMARY KEY ("categoryId", "subscriptionLevelId")
);
INSERT INTO "ProclamationCategorySubscriptionLevelLegacy" ("categoryId", "subscriptionLevelId")
SELECT j."categoryId", j."subscriptionLevelId"
FROM "ProclamationCategorySubscriptionLevel" AS j
JOIN "ProclamationCategory" AS c ON c."id" = j."categoryId" AND c."parentId" IS NOT NULL;
-- Remove former child categories (their data is archived above; the
-- subscription-level join rows for them are archived above and the
-- SubscriptionLevel table itself is left untouched).
DELETE FROM "ProclamationCategory" WHERE "parentId" IS NOT NULL;
-- Drop self-relation
ALTER TABLE "ProclamationCategory" DROP CONSTRAINT "ProclamationCategory_parentId_fkey";
-- DropIndex
DROP INDEX "ProclamationCategory_parentId_name_key";
-- AlterTable
ALTER TABLE "ProclamationCategory" DROP COLUMN "parentId";
-- CreateIndex
CREATE UNIQUE INDEX "ProclamationCategory_name_key" ON "ProclamationCategory"("name");
-- Subscription-level resources are intentionally left in place (unmanaged).
-- No DROP TABLE / DROP COLUMN / DROP CONSTRAINT is performed here, so this
-- migration never requires ownership of pre-existing subscription tables and
-- never destroys subscription data.
@@ -0,0 +1,28 @@
-- DropForeignKey
ALTER TABLE "LicenseKey" DROP CONSTRAINT "LicenseKey_userId_fkey";
-- DropIndex
DROP INDEX "Profile_userId_key";
-- AlterTable
ALTER TABLE "LicenseKey" ADD COLUMN "email" TEXT,
ALTER COLUMN "userId" DROP NOT NULL;
-- AlterTable
ALTER TABLE "Profile" ADD COLUMN "email" TEXT,
ALTER COLUMN "userId" DROP NOT NULL;
-- CreateIndex
CREATE INDEX "LicenseKey_userId_idx" ON "LicenseKey"("userId");
-- CreateIndex
CREATE INDEX "LicenseKey_email_idx" ON "LicenseKey"("email");
-- CreateIndex
CREATE INDEX "Profile_userId_idx" ON "Profile"("userId");
-- CreateIndex
CREATE INDEX "Profile_email_idx" ON "Profile"("email");
-- AddForeignKey
ALTER TABLE "LicenseKey" ADD CONSTRAINT "LicenseKey_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "user" ALTER COLUMN "roleId" SET DEFAULT 4;
@@ -0,0 +1,24 @@
-- Retain the legacy subscription artifacts as externally managed data.
--
-- SubscriptionLevel and ProclamationCategorySubscriptionLevel are no longer
-- modeled by the application. They are declared as external tables in
-- prisma.config.ts so Prisma Migrate ignores them entirely and never drops
-- them (production may not own these tables, and their data is kept for
-- recovery).
--
-- This migration only touches Prisma-managed tables to decouple them from the
-- legacy FK. It is intentionally idempotent so it applies cleanly to
-- production (columns present, NOT NULL, FK present), a fresh database built
-- from migration history, and development databases that already removed the
-- legacy objects.
-- Ensure the legacy columns exist. On databases built from migration history
-- these already exist; this re-adds them on environments that dropped them.
ALTER TABLE "Proclamation" ADD COLUMN IF NOT EXISTS "subscriptionLevelId" INTEGER;
ALTER TABLE "LicenseKey" ADD COLUMN IF NOT EXISTS "level" TEXT;
-- Decouple from SubscriptionLevel: drop the FK and relax NOT NULL so the
-- application can keep inserting rows without a legacy subscription value.
ALTER TABLE "Proclamation" DROP CONSTRAINT IF EXISTS "Proclamation_subscriptionLevelId_fkey";
ALTER TABLE "Proclamation" ALTER COLUMN "subscriptionLevelId" DROP NOT NULL;
ALTER TABLE "LicenseKey" ALTER COLUMN "level" DROP NOT NULL;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "LicenseKey" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "LicenseKey" ADD COLUMN "lastValidatedAt" TIMESTAMP(3);
@@ -0,0 +1,30 @@
-- CreateTable
CREATE TABLE "AuditLog" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userId" TEXT,
"email" TEXT,
"roleId" INTEGER,
"roleName" TEXT,
"page" TEXT NOT NULL,
"action" TEXT NOT NULL,
"entity" TEXT,
"entityId" TEXT,
"before" JSONB,
"after" JSONB,
"ip" TEXT,
CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "AuditLog_createdAt_idx" ON "AuditLog"("createdAt");
-- CreateIndex
CREATE INDEX "AuditLog_roleId_idx" ON "AuditLog"("roleId");
-- CreateIndex
CREATE INDEX "AuditLog_action_idx" ON "AuditLog"("action");
-- CreateIndex
CREATE INDEX "AuditLog_page_idx" ON "AuditLog"("page");
@@ -0,0 +1,53 @@
-- CreateTable
CREATE TABLE "Schedule" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Schedule_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Block" (
"id" TEXT NOT NULL,
"scheduleId" TEXT NOT NULL,
"startMinutes" INTEGER NOT NULL,
"endMinutes" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Block_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BlockProclamations" (
"blockId" TEXT NOT NULL,
"proclamationId" TEXT NOT NULL,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "BlockProclamations_pkey" PRIMARY KEY ("blockId","proclamationId")
);
-- CreateIndex
CREATE UNIQUE INDEX "Schedule_userId_key" ON "Schedule"("userId");
-- CreateIndex
CREATE INDEX "Block_scheduleId_idx" ON "Block"("scheduleId");
-- CreateIndex
CREATE INDEX "BlockProclamations_proclamationId_idx" ON "BlockProclamations"("proclamationId");
-- AddForeignKey
ALTER TABLE "Schedule" ADD CONSTRAINT "Schedule_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Block" ADD CONSTRAINT "Block_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "Schedule"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BlockProclamations" ADD CONSTRAINT "BlockProclamations_blockId_fkey" FOREIGN KEY ("blockId") REFERENCES "Block"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BlockProclamations" ADD CONSTRAINT "BlockProclamations_proclamationId_fkey" FOREIGN KEY ("proclamationId") REFERENCES "Proclamation"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Block" ADD COLUMN "dynamicAudio" BOOLEAN NOT NULL DEFAULT false;
@@ -0,0 +1,6 @@
-- AlterTable
ALTER TABLE "LicenseKey" ADD COLUMN "systemName" TEXT,
ADD COLUMN "systemNameLower" TEXT;
-- CreateIndex
CREATE UNIQUE INDEX "LicenseKey_userId_systemNameLower_key" ON "LicenseKey"("userId", "systemNameLower");
@@ -0,0 +1,46 @@
-- Phase 3: move Schedule from per-user to per-License-Key, add weekly support.
-- AlterTable: Schedule gains licenseKeyId + type, userId becomes optional.
ALTER TABLE "Schedule" ADD COLUMN "licenseKeyId" TEXT,
ADD COLUMN "type" TEXT;
-- AlterTable: Block gains day; new Blocks default dynamicAudio true.
ALTER TABLE "Block" ADD COLUMN "day" TEXT;
ALTER TABLE "Block" ALTER COLUMN "dynamicAudio" SET DEFAULT true;
-- DropIndex: userId is no longer the schedule identity.
DROP INDEX "Schedule_userId_key";
-- AlterTable: legacy owner becomes optional and non-unique.
ALTER TABLE "Schedule" ALTER COLUMN "userId" DROP NOT NULL;
-- CreateIndex
CREATE UNIQUE INDEX "Schedule_licenseKeyId_key" ON "Schedule"("licenseKeyId");
CREATE INDEX "Schedule_userId_idx" ON "Schedule"("userId");
-- AddForeignKey
ALTER TABLE "Schedule" ADD CONSTRAINT "Schedule_licenseKeyId_fkey" FOREIGN KEY ("licenseKeyId") REFERENCES "LicenseKey"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- Backfill: map each existing per-user Schedule to the account's oldest
-- License Key. Existing content was daily-only.
UPDATE "Schedule" s
SET "licenseKeyId" = lk."id",
"type" = COALESCE(s."type", 'daily')
FROM (
SELECT DISTINCT ON ("userId") "id", "userId"
FROM "LicenseKey"
WHERE "userId" IS NOT NULL
ORDER BY "userId", "createdAt" ASC, "id" ASC
) lk
WHERE s."userId" = lk."userId"
AND s."licenseKeyId" IS NULL;
-- Report (do not silently drop) schedules that cannot be mapped to a key.
DO $$
DECLARE unmapped integer;
BEGIN
SELECT count(*) INTO unmapped FROM "Schedule" WHERE "licenseKeyId" IS NULL;
IF unmapped > 0 THEN
RAISE NOTICE 'Phase 3 migration: % Schedule row(s) could not be mapped to a License Key (no owning user/license); retained with licenseKeyId NULL.', unmapped;
END IF;
END $$;
+146 -28
View File
@@ -21,13 +21,14 @@ model User {
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
roleId Int?
roleId Int? @default(4) // New users default to "User"
role UserRole? @relation(fields: [roleId], references: [id])
sessions Session[]
accounts Account[]
profile Profile?
profiles Profile[]
licenseKeys LicenseKey[]
userProclamations UserProclamations[]
schedules Schedule[]
@@unique([email])
@@map("user")
@@ -37,13 +38,17 @@ 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 {
@@ -123,9 +128,20 @@ 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])
blockProclamations BlockProclamations[]
// Legacy subscription-level FK. Retained in the database but unmanaged; the
// FK constraint is dropped and the value is left NULL. Hidden from the client.
subscriptionLevelId Int? @ignore
}
model ProclamationGroup {
id Int @id @default(autoincrement())
name String @unique
sortOrder Int @default(0)
proclamations Proclamation[]
}
model UserProclamations {
@@ -148,34 +164,61 @@ model SolfeggioFrequency {
model ProclamationCategory {
id Int @id @default(autoincrement())
name String
name String @unique
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 {
// Archive of the former child sub-categories removed when the
// ProclamationCategory hierarchy was flattened. Kept for recovery/audit.
// `@@ignore` keeps it out of the generated client while Prisma remains aware
// of the table so `migrate dev` does not report it as an untracked table.
model ProclamationCategoryLegacy {
id Int @id
name String
parentId Int?
sortOrder Int @default(0)
@@map("ProclamationCategoryLegacy")
@@ignore
}
// Archive of the category->subscription-level mappings for the child
// categories removed when the category hierarchy was flattened. Kept with
// `@@ignore` so Prisma remains aware of the table but it is excluded from the
// generated client and Prisma's own migrations.
model ProclamationCategorySubscriptionLevelLegacy {
categoryId Int
category ProclamationCategory @relation(fields: [categoryId], references: [id], onDelete: Cascade)
subscriptionLevelId Int
subscriptionLevel SubscriptionLevel @relation(fields: [subscriptionLevelId], references: [id], onDelete: Cascade)
@@id([categoryId, subscriptionLevelId])
@@map("ProclamationCategorySubscriptionLevelLegacy")
@@ignore
}
model LicenseKey {
id String @id @default(uuid())
key String @unique
active Boolean
level String
userId String
user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
lastValidatedAt DateTime?
userId String?
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
email String?
schedule Schedule?
// Human-readable label set only by the Kiosk (e.g. "Living Room"). Trimmed,
// 1-40 chars. Never a reference or foreign key; the License Key is the only
// identifier. `systemNameLower` is a case-folded copy used to enforce
// per-account, case-insensitive uniqueness when non-null.
systemName String?
systemNameLower String?
// Legacy subscription level. Retained in the database but unmanaged; hidden
// from the client and left NULL.
level String? @ignore
@@unique([userId, systemNameLower])
@@index([userId])
@@index([email])
}
model ApiKey {
@@ -187,14 +230,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`.
@@ -204,3 +239,86 @@ model ColorPalette {
@@map("ColorPalette")
}
// Append-only audit trail of database mutations. Written centrally by the
// Prisma extension in src/lib/db.ts; read by the admin Audit Log page.
model AuditLog {
id String @id @default(uuid())
createdAt DateTime @default(now())
userId String?
email String?
roleId Int?
roleName String?
page String
action String
entity String?
entityId String?
before Json?
after Json?
ip String?
@@index([createdAt])
@@index([roleId])
@@index([action])
@@index([page])
@@map("AuditLog")
}
// A listening schedule assigned to one License Key (at most one per key).
// `type` is "daily" | "weekly" | null (null = no schedule). Times are
// wall-clock minutes from midnight (0-1440), interpreted by the kiosk in its
// own configured timezone. The portal never stores a timezone.
//
// `userId` is a legacy owner retained while per-user schedules are migrated to
// per-license-key; new schedules set `licenseKeyId` and leave `userId` null.
model Schedule {
id String @id @default(uuid())
licenseKeyId String? @unique
licenseKey LicenseKey? @relation(fields: [licenseKeyId], references: [id], onDelete: Cascade)
userId String?
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
type String?
blocks Block[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
}
// A single time range within a Schedule, plus the ordered set of selected
// Proclamations to play during it. `day` is null for daily schedules and
// "mon".."sun" for weekly schedules. Overnight (midnight-crossing) ranges are
// valid: an end clock time before the start belongs to the start day and spills
// into the next day.
model Block {
id String @id @default(uuid())
scheduleId String
schedule Schedule @relation(fields: [scheduleId], references: [id], onDelete: Cascade)
day String?
startMinutes Int
endMinutes Int
// When true, the kiosk enables dynamic audio for the duration of this block,
// overriding any audio selections made on the device. Portal default: true.
dynamicAudio Boolean @default(true)
proclamations BlockProclamations[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([scheduleId])
}
// Join table: the Proclamations selected for a Block. Carries the Proclamation
// content for playback via the API; the portal UI only ever reads
// title/category/group.
model BlockProclamations {
blockId String
block Block @relation(fields: [blockId], references: [id], onDelete: Cascade)
proclamationId String
proclamation Proclamation @relation(fields: [proclamationId], references: [id])
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@id([blockId, proclamationId])
@@index([proclamationId])
}
+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({
+163
View File
@@ -0,0 +1,163 @@
import 'dotenv/config';
// Exercises the Kiosk-facing HTTP API exactly as the Kiosk client does.
// Scenarios: b (validate + name), c (duplicate name conflict), k (ETag/304),
// old-client backward compatibility.
const BASE = process.env.P5_BASE ?? 'http://localhost:5173';
const KA1 = 'ALCE-AAAA-AAAA-0001';
const KA2 = 'ALCE-AAAA-AAAA-0002';
const KB1 = 'BOBB-BBBB-BBBB-0001';
const EMAIL = 'p5-alice@example.com';
const results: { id: string; pass: boolean; note: string }[] = [];
function rec(id: string, pass: boolean, note: string) {
results.push({ id, pass, note });
console.log(`${pass ? 'PASS' : 'FAIL'} ${id} ${note}`);
}
async function post(path: string, body: unknown) {
const res = await fetch(BASE + path, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body)
});
return { status: res.status, body: await res.json().catch(() => null), headers: res.headers };
}
async function put(path: string, body: unknown) {
const res = await fetch(BASE + path, {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body)
});
return { status: res.status, body: await res.json().catch(() => null) };
}
async function get(path: string, headers: Record<string, string> = {}) {
const res = await fetch(BASE + path, { headers });
return { status: res.status, body: await res.json().catch(() => null), headers: res.headers };
}
async function main() {
// (b) validate without a name first (old-client behavior), then set a name.
const validateNoName = await post('/api/v2/license', {
license_key: KA1,
email_address: EMAIL
});
rec(
'b1',
validateNoName.status === 200 && validateNoName.body?.systemNameStatus === 'unchanged',
`validation without systemName -> ${validateNoName.status}, status=${validateNoName.body?.systemNameStatus}`
);
const setName = await post('/api/v2/license', {
license_key: KA1,
email_address: EMAIL,
systemName: 'Living Room'
});
rec(
'b2',
setName.status === 200 && setName.body?.systemNameStatus === 'ok',
`name a Kiosk -> ${setName.status}, status=${setName.body?.systemNameStatus}`
);
// (c) duplicate name, different casing, same account -> conflict + not cleared.
const dup = await post('/api/v2/license', {
license_key: KA2,
email_address: EMAIL,
systemName: 'living room'
});
rec(
'c1',
dup.status === 200 && dup.body?.systemNameStatus === 'conflict',
`case-insensitive duplicate -> status=${dup.body?.systemNameStatus} (validation still ${dup.status})`
);
// Confirm the original name survived and KA2 has no name.
const after = await get('/api/v2/licenses/' + KA1 + '/schedule');
rec('c2', after.status === 200, `KA1 schedule GET after conflict -> ${after.status}`);
// (c) PUT system-name endpoint: 409 for a taken name.
const putDup = await put('/api/v2/licenses/' + KA2 + '/system-name', {
systemName: 'LIVING ROOM'
});
rec(
'c3',
putDup.status === 409 && putDup.body?.message === 'name_taken',
`PUT duplicate -> ${putDup.status} ${putDup.body?.message}`
);
const putOk = await put('/api/v2/licenses/' + KA2 + '/system-name', { systemName: 'Kitchen' });
rec('c4', putOk.status === 200, `PUT unique name -> ${putOk.status}`);
const putInvalid = await put('/api/v2/licenses/' + KA2 + '/system-name', { systemName: ' ' });
rec(
'c5',
putInvalid.status === 400 && putInvalid.body?.message === 'invalid_name',
`PUT blank -> ${putInvalid.status} ${putInvalid.body?.message}`
);
const putUnknown = await put('/api/v2/licenses/ZZZZ-ZZZZ-ZZZZ-ZZZZ/system-name', {
systemName: 'X'
});
rec(
'c6',
putUnknown.status === 404 && putUnknown.body?.message === 'unknown_license',
`PUT unknown license -> ${putUnknown.status} ${putUnknown.body?.message}`
);
// (k) schedule GET parity between the legacy endpoint and the new key-only endpoint.
const modern = await get('/api/v2/licenses/' + KA1 + '/schedule');
const legacy = await get(
'/api/v2/schedule?email=' + encodeURIComponent(EMAIL) + '&licenseKey=' + KA1
);
rec(
'k1',
modern.status === 200 && legacy.status === 200 && modern.body?.version === legacy.body?.version,
`modern vs legacy version equal=${modern.body?.version === legacy.body?.version}`
);
// (k) conditional GET: matching If-None-Match -> 304.
const etag = modern.headers.get('etag') ?? '';
const notModified = await get('/api/v2/licenses/' + KA1 + '/schedule', { 'if-none-match': etag });
rec('k2', notModified.status === 304, `conditional GET with ETag -> ${notModified.status}`);
const stale = await get('/api/v2/licenses/' + KA1 + '/schedule', { 'if-none-match': '"stale"' });
rec('k3', stale.status === 200, `conditional GET with stale ETag -> ${stale.status}`);
// Unknown license -> 404.
const missing = await get('/api/v2/licenses/ZZZZ-ZZZZ-ZZZZ-ZZZZ/schedule');
rec('k4', missing.status === 404, `unknown license schedule GET -> ${missing.status}`);
// Old-client validation (no systemName field at all) never clears the name.
const validateAgain = await post('/api/v2/license', { license_key: KA1, email_address: EMAIL });
const checkName = await put('/api/v2/licenses/' + KA1 + '/system-name', {
systemName: 'Living Room'
});
rec(
'old1',
validateAgain.status === 200 &&
validateAgain.body?.systemNameStatus === 'unchanged' &&
checkName.status === 200,
`old-client validate stays unchanged; name still present`
);
// Old-client list endpoint shape: blocks array still present.
rec(
'old2',
legacy.status === 200 && Array.isArray(legacy.body?.blocks),
`legacy endpoint still returns email+blocks (${legacy.body?.blocks?.length} blocks)`
);
// Cross-account: Bob's key with Alice's email must fail.
const cross = await get(
'/api/v2/schedule?email=' + encodeURIComponent(EMAIL) + '&licenseKey=' + KB1
);
rec('auth1', cross.status === 400, `cross-account legacy schedule -> ${cross.status}`);
console.log('\nP5_HTTP_RESULTS ' + JSON.stringify(results));
}
main().catch((e) => {
console.error('RUNNER ERROR', e);
process.exit(1);
});
+45
View File
@@ -0,0 +1,45 @@
import 'dotenv/config';
import pg from 'pg';
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '../src/lib/generated/prisma/client';
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const prisma = new PrismaClient({ adapter: new PrismaPg(pool) });
// A handful of real Proclamations so schedule Blocks have valid references.
async function main() {
const category = await prisma.proclamationCategory.findFirst({ orderBy: { id: 'asc' } });
const group = await prisma.proclamationGroup.findFirst({ orderBy: { id: 'asc' } });
const freq = await prisma.solfeggioFrequency.findFirst({ orderBy: { id: 'asc' } });
if (!category || !group || !freq) throw new Error('seed missing catalog');
const names = ['P5 Dawn', 'P5 Dusk', 'P5 Noon', 'P5 Night'];
const ids: string[] = [];
for (const name of names) {
const existing = await prisma.proclamation.findUnique({ where: { name } });
if (existing) {
ids.push(existing.id);
continue;
}
const created = await prisma.proclamation.create({
data: {
name,
content: `<p>${name} content</p>`,
categoryId: category.id,
groupId: group.id,
solfeggioFrequencyId: freq.id
}
});
ids.push(created.id);
}
console.log(JSON.stringify(ids));
await prisma.$disconnect();
await pool.end();
}
main().catch(async (e) => {
console.error(e);
await prisma.$disconnect();
await pool.end();
process.exit(1);
});
+49
View File
@@ -0,0 +1,49 @@
import 'dotenv/config';
import pg from 'pg';
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '../src/lib/generated/prisma/client';
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({ adapter });
// Two accounts, each with two License Keys, for scenario coverage:
// - acct A: keys KA1, KA2 (same account -> name uniqueness + copy)
// - acct B: key KB1 (cross-account isolation)
// Names are left NULL; the Kiosk sets them via the API.
const accounts = [
{ email: 'p5-alice@example.com', key: 'ALCE-AAAA-AAAA-0001' },
{ email: 'p5-alice@example.com', key: 'ALCE-AAAA-AAAA-0002' },
{ email: 'p5-bob@example.com', key: 'BOBB-BBBB-BBBB-0001' }
];
async function ensureUser(email) {
let user = await prisma.user.findUnique({ where: { email } });
if (!user) {
user = await prisma.user.create({
data: { id: crypto.randomUUID(), name: email.split('@')[0], email, emailVerified: true }
});
}
return user;
}
async function main() {
for (const { email, key } of accounts) {
const user = await ensureUser(email);
const existing = await prisma.licenseKey.findUnique({ where: { key } });
if (!existing) {
await prisma.licenseKey.create({ data: { key, active: true, userId: user.id } });
}
}
console.log('P5 test data ready:');
for (const { email, key } of accounts) console.log(` ${email} -> ${key}`);
await prisma.$disconnect();
await pool.end();
}
main().catch(async (e) => {
console.error(e);
await prisma.$disconnect();
await pool.end();
process.exit(1);
});
+21
View File
@@ -0,0 +1,21 @@
import 'dotenv/config';
import pg from 'pg';
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '../src/lib/generated/prisma/client';
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const prisma = new PrismaClient({ adapter: new PrismaPg(pool) });
const schedules = await prisma.schedule.findMany({ include: { blocks: true } });
const keys = await prisma.licenseKey.findMany({ select: { key: true, systemName: true } });
console.log(
JSON.stringify({
scheduleCount: schedules.length,
blockCount: schedules.reduce((n, s) => n + s.blocks.length, 0),
types: schedules.map((s) => s.type),
names: keys
.filter((k) => k.systemName)
.map((k) => k.systemName)
.sort()
})
);
await prisma.$disconnect();
await pool.end();
+3 -3
View File
@@ -1,11 +1,11 @@
import type { Session, User } from 'better-auth';
import type { auth } from '$lib/auth';
import type { ApiKeyRecord } from '$lib/server/api-key';
declare global {
namespace App {
interface Locals {
session: Session | null;
user: User | null;
session: typeof auth.$Infer.Session.session | null;
user: typeof auth.$Infer.Session.user | null;
apiKey?: ApiKeyRecord;
}
}
+8 -8
View File
@@ -5,21 +5,21 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function () {
try {
const theme = localStorage.getItem('theme');
if (
const dark =
theme === 'dark' ||
(!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
(!theme && window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.classList.toggle('dark', dark);
} catch (e) {
// ignore
}
})();
</script>
%sveltekit.head%
</head>
<body class="m-0" data-sveltekit-preload-data="hover">
<div class="flex h-dvh w-dvw flex-col">%sveltekit.body%</div>
<body data-sveltekit-preload-data="hover">
<div>%sveltekit.body%</div>
</body>
</html>
+21
View File
@@ -3,8 +3,24 @@ import { svelteKitHandler } from 'better-auth/svelte-kit';
import { building } from '$app/environment';
import { env } from '$env/dynamic/private';
import { validateApiKey } from '$lib/server/api-key';
import { runWithAuditContext, type AuditContext } from '$lib/server/audit-context';
import { clientIp } from '$lib/server/v2-helpers';
function contextFor(
event: { url: URL; request: Request },
session?: App.Locals['user']
): AuditContext {
return {
userId: session?.id ?? null,
email: session?.email ?? null,
roleId: session?.roleId ?? null,
page: event.url.pathname,
ip: clientIp(event.request)
};
}
export async function handle({ event, resolve }) {
return runWithAuditContext(contextFor(event), async () => {
// v2 auth is ALWAYS enforced — no API_KEYS_ACTIVE bypass. The only
// exceptions are the signed onboarding routes (HMAC-protected).
// TODO: re-enable when v2 API key auth is ready.
@@ -67,7 +83,12 @@ export async function handle({ event, resolve }) {
if (session) {
event.locals.session = session.session;
event.locals.user = session.user;
return runWithAuditContext(contextFor(event, session.user), () =>
svelteKitHandler({ event, resolve, auth, building })
);
}
return svelteKitHandler({ event, resolve, auth, building });
});
}
+36 -1
View File
@@ -5,13 +5,40 @@ import { sveltekitCookies } from 'better-auth/svelte-kit';
import { getRequestEvent } from '$app/server';
import { sendMail } from './server/resend';
import { sendVerificationEmail } from './server/email-verification';
import { sendRegistrationEmail } from './server/registration-email';
import { env } from '$env/dynamic/private';
/**
* An admin-created user is invited with a set-password link. That reuses the
* password-reset token flow but marks the request with `invite=1` so we send a
* welcome/registration email instead of a "you requested a reset" email.
*/
function isRegistrationInvite(url: string): boolean {
try {
const callbackURL = new URL(url).searchParams.get('callbackURL') ?? '';
return callbackURL.includes('invite=1');
} catch {
return false;
}
}
export const auth = betterAuth({
baseURL: env.BETTER_AUTH_URL,
database: prismaAdapter(prisma, {
provider: 'postgresql'
}),
user: {
// Expose our custom role column on the session user so role checks can
// read `locals.user.roleId` without a database round-trip.
additionalFields: {
roleId: {
type: 'number',
required: false,
defaultValue: 4,
input: false
}
}
},
emailVerification: {
sendOnSignUp: true,
sendVerificationEmail: async ({ user, token }) => {
@@ -27,8 +54,16 @@ export const auth = betterAuth({
enabled: true,
requireEmailVerification: true,
sendResetPassword: async ({ user, url }) => {
if (isRegistrationInvite(url)) {
void sendRegistrationEmail({ email: user.email, url }).catch((e) => {
console.error('sendRegistrationEmail template email failed:', e);
});
return;
}
void sendMail({
to: user.email,
subject: 'Reset your Radiant Wave password',
templateId: 'password-reset-email',
data: {
target_email_address: user.email,
@@ -51,6 +86,6 @@ export const auth = betterAuth({
window: 60,
max: 100
},
trustedOrigins: ['http://localhost:5173', 'https://portal.rhemawavetech.com'],
trustedOrigins: ['http://localhost:5173', 'https://portal.radiantwavetech.com'],
plugins: [sveltekitCookies(getRequestEvent)]
});
@@ -0,0 +1,25 @@
<script lang="ts">
import type { Snippet } from 'svelte';
let {
title,
description = '',
children
}: {
title: string;
description?: string;
children: Snippet;
} = $props();
</script>
<section class="mt-8">
<div>
<h2 class="text-lg font-semibold text-slate-900 dark:text-slate-100">{title}</h2>
{#if description}
<p class="mt-1 text-sm text-slate-500 dark:text-slate-400">{description}</p>
{/if}
</div>
<div class="mt-4">
{@render children()}
</div>
</section>
+219
View File
@@ -0,0 +1,219 @@
<script lang="ts">
export type DataTableColumn = {
label: string;
value: (record: Record<string, unknown>) => unknown;
};
let {
title = '',
columns = [] as DataTableColumn[],
records = [] as Record<string, unknown>[],
newLabel = 'New',
searchPlaceholder = 'Search...',
copyableFields = [] as string[],
draggable = false,
onRowClick,
onNew,
onReorder
}: {
title?: string;
columns?: DataTableColumn[];
records?: Record<string, unknown>[];
newLabel?: string;
searchPlaceholder?: string;
copyableFields?: string[];
draggable?: boolean;
onRowClick?: (record: Record<string, unknown>) => void;
onNew?: () => void;
onReorder?: (ids: (string | number)[]) => void;
} = $props();
let search = $state('');
const filteredRecords = $derived(
search
? records.filter((record) =>
columns.some((column) =>
String(column.value(record) ?? '')
.toLowerCase()
.includes(search.toLowerCase())
)
)
: records
);
let dragOverride = $state<Record<string, unknown>[] | null>(null);
let dragIndex = $state<number | null>(null);
let hoverIndex = $state<number | null>(null);
const rows = $derived(dragOverride ?? filteredRecords);
function rowKey(record: Record<string, unknown>, index: number): string {
return String(record.id ?? record.key ?? index);
}
function copyValue(value: unknown) {
navigator.clipboard.writeText(String(value ?? ''));
}
function handleDragStart(e: DragEvent, i: number) {
dragOverride = filteredRecords;
dragIndex = i;
hoverIndex = i;
e.dataTransfer!.effectAllowed = 'move';
e.dataTransfer!.setData('text/plain', '');
}
function handleDragOver(e: DragEvent, i: number) {
if (dragIndex === null) return;
e.preventDefault();
if (i === hoverIndex) return;
e.dataTransfer!.dropEffect = 'move';
const reordered = rows.slice();
const [moved] = reordered.splice(dragIndex, 1);
reordered.splice(i, 0, moved);
dragOverride = reordered;
dragIndex = i;
hoverIndex = i;
}
async function handleDrop() {
if (dragIndex === null) return;
const ids = rows.map((r) => r.id as string | number);
await onReorder?.(ids);
dragIndex = null;
hoverIndex = null;
}
function handleDragEnd() {
dragOverride = null;
dragIndex = null;
hoverIndex = null;
}
</script>
<div class="mx-auto w-full">
<div class="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 class="text-xl font-semibold text-slate-900 dark:text-slate-100">{title}</h2>
<div class="flex w-full flex-col gap-3 sm:w-auto sm:flex-row sm:items-center">
<input
type="search"
bind:value={search}
placeholder={searchPlaceholder}
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-indigo-500 focus:outline-none sm:w-64 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
/>
{#if onNew}
<button
type="button"
onclick={onNew}
class="inline-flex h-10 items-center justify-center gap-1.5 rounded-md bg-indigo-600 px-4 text-sm font-medium text-white shadow-sm transition hover:bg-indigo-700"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"><path d="M12 5v14" /><path d="M5 12h14" /></svg
>
{newLabel}
</button>
{/if}
</div>
</div>
<div
class="w-full overflow-x-auto rounded-lg border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
>
<table class="w-full min-w-max text-left text-sm text-slate-700 dark:text-slate-300">
<thead>
<tr
class="border-b border-slate-200 bg-slate-100 text-xs font-semibold tracking-wide text-slate-500 dark:border-slate-800 dark:bg-slate-800 dark:text-slate-400"
>
{#if draggable}
<th scope="col" class="w-10 px-4 py-3"></th>
{/if}
{#each columns as column (column.label)}
<th scope="col" class="px-4 py-3">{column.label}</th>
{/each}
</tr>
</thead>
<tbody>
{#each rows as record, i (rowKey(record, i))}
<tr
class="transition {i % 2 === 0
? 'bg-white dark:bg-slate-900'
: 'bg-slate-50 dark:bg-slate-950'} {onRowClick
? 'cursor-pointer hover:bg-indigo-50 dark:hover:bg-indigo-950'
: ''}"
{draggable}
ondragstart={(e) => handleDragStart(e, i)}
ondragover={(e) => handleDragOver(e, i)}
ondrop={handleDrop}
ondragend={handleDragEnd}
onclick={() => onRowClick?.(record)}
>
{#if draggable}
<td class="px-4 py-3" onclick={(e) => e.stopPropagation()} aria-hidden="true">
<span
class="cursor-grab text-slate-400 select-none active:cursor-grabbing dark:text-slate-500"
title="Drag to reorder">⠿</span
>
</td>
{/if}
{#each columns as column (column.label)}
<td class="px-4 py-3 whitespace-nowrap">
{#if copyableFields.includes(column.label) && column.value(record) != null}
<span>
{String(column.value(record))}
<button
type="button"
onclick={(e) => {
e.stopPropagation();
copyValue(column.value(record));
}}
title="Copy to clipboard"
class="ml-1 inline-flex text-slate-400 transition hover:text-indigo-600 dark:text-slate-500 dark:hover:text-indigo-400"
>
<svg
viewBox="0 0 16 16"
width="14"
height="14"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<rect x="3.5" y="2.5" width="11" height="11" rx="1" />
<path d="M5.5 2.5V1.5a1 1 0 011-1h3a1 1 0 011 1v1" />
</svg>
</button>
</span>
{:else}
{String(column.value(record) ?? '')}
{/if}
</td>
{/each}
</tr>
{:else}
<tr>
<td
colspan={columns.length + (draggable ? 1 : 0)}
class="bg-white dark:bg-slate-900 px-4 py-10 text-center text-slate-500 dark:text-slate-400"
>
No results found.
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
+103
View File
@@ -0,0 +1,103 @@
<script lang="ts">
import {
fiveMinuteOptions,
formatTime,
hmToMinutes,
parseTimeInput,
toHm
} from '$lib/schedule-utils';
// A reusable 12-hour clock input. Displays and accepts AM/PM times, offers
// the 5-minute options as suggestions, and allows typing any minute 00-59.
// Its value model is a 24-hour "HH:MM" string.
let {
value = '00:00',
label,
onchange,
onvalidity,
required = false,
disabled = false
}: {
value?: string;
label?: string;
onchange?: (value: string) => void;
onvalidity?: (valid: boolean) => void;
required?: boolean;
disabled?: boolean;
} = $props();
const inputId = $props.id();
const options = fiveMinuteOptions();
// While the user is typing we show their raw text (`editing`); otherwise we
// show the canonical 12-hour label derived from the 24-hour value.
let editing = $state<string | null>(null);
const valueMinutes = $derived(hmToMinutes(value) ?? 0);
const display = $derived(editing ?? formatTime(valueMinutes));
const error = $derived.by(() => {
if (editing === null) return '';
const result = parseTimeInput(editing);
return result.ok ? '' : result.error;
});
function handleInput(event: Event) {
const raw = (event.currentTarget as HTMLInputElement).value;
editing = raw;
const result = parseTimeInput(raw);
if (result.ok) {
onvalidity?.(true);
onchange?.(toHm(result.minutes));
} else {
onvalidity?.(false);
}
}
function handleBlur() {
if (editing === null) return;
const result = parseTimeInput(editing);
if (result.ok) {
// Drop back to the canonical label derived from the value.
editing = null;
onvalidity?.(true);
}
}
function handleFocus(event: FocusEvent) {
editing = (event.currentTarget as HTMLInputElement).value;
}
</script>
<div class="flex flex-col gap-1">
{#if label}
<label for={inputId} class="text-sm font-medium text-slate-700 dark:text-slate-300">
{label}
</label>
{/if}
<input
id={inputId}
type="text"
inputmode="text"
autocomplete="off"
class="rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm focus:border-indigo-500 focus:outline-none disabled:opacity-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
list={`${inputId}-options`}
{required}
{disabled}
aria-label={label}
aria-invalid={error ? 'true' : undefined}
aria-describedby={error ? `${inputId}-error` : undefined}
value={display}
oninput={handleInput}
onfocus={handleFocus}
onblur={handleBlur}
/>
<datalist id={`${inputId}-options`}>
{#each options as minutes (minutes)}
<option value={formatTime(minutes)}></option>
{/each}
</datalist>
{#if error}
<p id={`${inputId}-error`} role="alert" class="text-sm text-red-600 dark:text-red-400">
{error}
</p>
{/if}
</div>
@@ -0,0 +1,66 @@
import { page } from 'vitest/browser';
import { describe, it, expect, vi } from 'vitest';
import { render } from 'vitest-browser-svelte';
import TimeInput from './TimeInput.svelte';
function mount(props: Record<string, unknown> = {}) {
return render(TimeInput, { value: '07:05', label: 'Time', ...props });
}
async function type(text: string) {
const input = (await page.getByRole('combobox', { name: 'Time' }).element()) as HTMLInputElement;
input.value = text;
input.dispatchEvent(new Event('input', { bubbles: true }));
}
describe('TimeInput', () => {
it('displays the 24-hour value as a 12-hour label', async () => {
mount({ value: '21:05' });
await expect.element(page.getByRole('combobox', { name: 'Time' })).toHaveValue('9:05 PM');
});
it('calls onchange with a 24-hour value for tolerant input', async () => {
const onchange = vi.fn();
mount({ onchange });
await type('7:05 pm');
expect(onchange).toHaveBeenCalledWith('19:05');
});
it('accepts typed minutes that are not 5-minute steps', async () => {
const onchange = vi.fn();
mount({ value: '00:00', onchange });
await type('7:07');
expect(onchange).toHaveBeenCalledWith('07:07');
});
it('accepts an hour with no minutes', async () => {
const onchange = vi.fn();
mount({ onchange });
await type('7pm');
expect(onchange).toHaveBeenCalledWith('19:00');
});
it('shows clear feedback and reports invalid input', async () => {
const onvalidity = vi.fn();
mount({ onvalidity });
await type('13:00 PM');
await expect.element(page.getByRole('alert')).toBeInTheDocument();
expect(onvalidity).toHaveBeenCalledWith(false);
});
it('offers the 5-minute clock options', async () => {
mount();
const options = document.querySelectorAll('datalist option');
expect(options).toHaveLength((24 * 60) / 5);
});
});
+19 -14
View File
@@ -17,8 +17,8 @@
deleteAction?: string;
} = $props();
function handleBackdrop(e: MouseEvent) {
if (e.target === e.currentTarget) onClose?.();
function handleBackdrop() {
onClose?.();
}
function handleKeydown(e: KeyboardEvent) {
@@ -44,24 +44,27 @@
{#if open}
<div
role="presentation"
class="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/50 pt-12"
onclick={handleBackdrop}
class="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/50 p-4"
onkeydown={handleKeydown}
role="presentation"
>
<div class="absolute inset-0" onclick={handleBackdrop} aria-hidden="true"></div>
<div
role="dialog"
aria-modal="true"
aria-label={title}
class="relative z-10 flex max-h-[90vh] w-full max-w-2xl flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-xl dark:border-slate-800 dark:bg-slate-900"
>
<div
class="my-8 w-full max-w-2xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900"
class="flex shrink-0 items-center justify-between gap-3 border-b border-slate-200 px-6 py-4 dark:border-slate-800"
>
<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>
<h2 class="text-lg font-bold text-slate-900 dark:text-slate-100">{title}</h2>
<div class="flex items-center gap-2">
{#if canDelete && recordId}
<button
type="button"
onclick={confirmDelete}
class="px-3 py-1 text-sm text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
class="rounded-md bg-red-50 px-3 py-1.5 text-sm font-medium text-red-600 ring-1 ring-red-200 transition ring-inset hover:bg-red-600 hover:text-white dark:bg-red-950/50 dark:text-red-400 dark:ring-red-900"
>
Delete
</button>
@@ -69,12 +72,14 @@
<button
type="button"
onclick={onClose}
class="px-2 py-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>&times;</button
aria-label="Close"
class="rounded-md p-1.5 text-2xl leading-none text-slate-400 transition hover:bg-slate-100 hover:text-red-600 dark:text-slate-500 dark:hover:bg-slate-800"
>
&times;
</button>
</div>
</div>
<div class="px-6 py-4">
<div class="overflow-y-auto px-6 py-5">
{@render children()}
</div>
</div>
-248
View File
@@ -1,248 +0,0 @@
<script lang="ts">
import type { PrismaScalarType } from '$lib/svelte-admin/types';
import { fieldNameToLabel, resolveFieldValue } from '$lib/svelte-admin/introspect';
let {
records = [],
listFields = [],
searchFields = [],
fieldTypes = {} as Record<string, PrismaScalarType>,
singular = 'record',
draggable = false,
copyableFields = [] as string[],
onRowClick,
onNew,
onReorder
}: {
records: Record<string, unknown>[];
listFields: string[];
searchFields?: string[];
fieldTypes?: Record<string, PrismaScalarType>;
singular?: string;
draggable?: boolean;
copyableFields?: string[];
onRowClick?: (record: Record<string, unknown>) => void;
onNew?: () => void;
onReorder?: (ids: (string | number)[]) => void;
} = $props();
let search = $state('');
let sortCol = $state<string | null>(null);
let sortAsc = $state(true);
let filtered = $derived(
search
? records.filter((r) =>
searchFields.some((f) => {
const v = resolveFieldValue(r, f);
return v != null && String(v).toLowerCase().includes(search.toLowerCase());
})
)
: records
);
let sorted = $derived(
sortCol
? [...filtered].sort((a, b) => {
const va = resolveFieldValue(a, sortCol!);
const vb = resolveFieldValue(b, sortCol!);
if (va == null) return 1;
if (vb == null) return -1;
const cmp = String(va).localeCompare(String(vb), undefined, { numeric: true });
return sortAsc ? cmp : -cmp;
})
: filtered
);
let isDragging = $state(false);
let dragOverride = $state<Record<string, unknown>[] | null>(null);
let rows = $derived(dragOverride ?? sorted);
let dragIndex = $state<number | null>(null);
let hoverIndex = $state<number | null>(null);
function toggleSort(field: string) {
if (sortCol === field) {
sortAsc = !sortAsc;
} else {
sortCol = field;
sortAsc = true;
}
}
function handleDragStart(e: DragEvent, i: number) {
dragOverride = sorted;
isDragging = true;
dragIndex = i;
hoverIndex = i;
e.dataTransfer!.effectAllowed = 'move';
e.dataTransfer!.setData('text/plain', '');
}
function handleDragOver(e: DragEvent, i: number) {
if (dragIndex === null) return;
e.preventDefault();
if (i === hoverIndex) return;
e.dataTransfer!.dropEffect = 'move';
const reordered = rows.slice();
const [moved] = reordered.splice(dragIndex, 1);
reordered.splice(i, 0, moved);
dragOverride = reordered;
dragIndex = i;
hoverIndex = i;
}
async function handleDrop() {
if (dragIndex === null) return;
const ids = rows.map((r) => r.id as string | number);
await onReorder?.(ids);
isDragging = false;
dragIndex = null;
hoverIndex = null;
}
function handleDragEnd() {
dragOverride = null;
isDragging = false;
dragIndex = null;
hoverIndex = null;
}
function copyValue(v: unknown) {
navigator.clipboard.writeText(String(v ?? ''));
}
function formatCellValue(v: unknown, field: string): string {
if (v == null) return '';
const ft = fieldTypes[field];
if (ft === 'DateTime') {
if (v instanceof Date) return v.toLocaleString();
return new Date(v as string).toLocaleString();
}
if (ft === 'Boolean') return v ? 'Active' : 'Inactive';
if (ft === 'Float') {
const n = Number(v);
return Number.isNaN(n) ? String(v) : n.toFixed(2);
}
return String(v);
}
</script>
<div class="flex flex-col gap-4">
<div class="flex items-center justify-between gap-4">
{#if searchFields.length > 0}
<input
type="text"
placeholder="Search {singular}s..."
bind:value={search}
class="max-w-xs border border-gray-300 px-3 py-1.5 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
/>
{:else}
<div></div>
{/if}
{#if onNew}
<button
type="button"
onclick={onNew}
class="border border-gray-300 px-3 py-1.5 text-sm font-medium dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
>
New {singular} +
</button>
{/if}
</div>
<div class="overflow-x-auto border border-gray-200 dark:border-gray-700">
<table class="w-full">
<thead>
<tr class="border-b border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-800">
{#if draggable}
<th scope="col" class="w-10 px-2 py-2"></th>
{/if}
{#each listFields as field (field)}
<th
scope="col"
onclick={() => toggleSort(field)}
class="cursor-pointer px-4 py-2 text-left text-xs font-medium tracking-wider text-gray-500 uppercase hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
{fieldNameToLabel(field)}
{#if sortCol === field}
<span class="ml-1">{sortAsc ? '\u25B2' : '\u25BC'}</span>
{/if}
</th>
{/each}
</tr>
</thead>
<tbody>
{#each rows as record, i ((record['id'] as string) ?? (record['key'] as string) ?? i)}
<tr
{draggable}
ondragstart={(e) => handleDragStart(e, i)}
ondragover={(e) => handleDragOver(e, i)}
ondrop={handleDrop}
ondragend={handleDragEnd}
onclick={() => onRowClick?.(record)}
class="border-b border-gray-200 hover:bg-gray-100 dark:border-gray-700 dark:text-gray-100 dark:hover:bg-gray-800 {draggable &&
isDragging &&
dragIndex === i
? 'opacity-40'
: ''} {draggable && isDragging && hoverIndex === i
? 'border-t-2 border-t-blue-500'
: ''}"
>
{#if draggable}
<td
class="w-10 cursor-grab px-2 py-2 text-center"
onclick={(e) => e.stopPropagation()}
>
<span class="text-gray-400 select-none">⠿</span>
</td>
{/if}
{#each listFields as field (field)}
<td class="px-4 py-2 text-sm">
<span class="inline-flex items-center gap-1.5">
{formatCellValue(resolveFieldValue(record, field), field)}
{#if copyableFields.includes(field)}
<button
type="button"
onclick={(e) => {
e.stopPropagation();
copyValue(resolveFieldValue(record, field));
}}
title="Copy to clipboard"
class="shrink-0 leading-none text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<svg
class="size-3.5"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="3.5" y="2.5" width="11" height="11" rx="1" />
<path d="M5.5 2.5V1.5a1 1 0 011-1h3a1 1 0 011 1v1" />
</svg>
</button>
{/if}
</span>
</td>
{/each}
</tr>
{:else}
<tr>
<td
colspan={listFields.length + (draggable ? 1 : 0)}
class="px-4 py-8 text-center text-sm text-gray-500 dark:text-gray-400"
>
No {singular}s found.
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
+11 -15
View File
@@ -1,11 +1,5 @@
<script lang="ts">
let {
message = '',
type = 'success' as 'success' | 'error'
}: {
message?: string;
type?: 'success' | 'error';
} = $props();
let { message = '' }: { message?: string } = $props();
let remaining = $state(5);
let visible = $state(true);
@@ -32,15 +26,17 @@
{#if visible}
<div
role="alert"
class="fixed bottom-4 left-4 z-50 flex min-w-64 items-center gap-3 border px-4 py-3 text-sm shadow-lg {type ===
'success'
? 'border-green-600 bg-green-50 text-green-800'
: 'border-red-600 bg-red-50 text-red-800'}"
class="fixed right-4 bottom-4 z-50 flex items-center gap-3 rounded-lg border border-slate-700 bg-slate-900 px-4 py-3 text-sm text-white shadow-lg dark:border-slate-300 dark:bg-slate-950"
>
<span class="flex-1">{message}</span>
<span class="text-xs tabular-nums opacity-70">{remaining}s</span>
<button type="button" onclick={dismiss} class="ml-1 leading-none font-bold hover:opacity-70"
>&times;</button
<span>{message}</span>
<span class="text-xs text-slate-400 dark:text-slate-500">{remaining}s</span>
<button
type="button"
onclick={dismiss}
aria-label="Dismiss"
class="rounded p-1 text-slate-400 transition hover:text-white dark:text-slate-500"
>
&times;
</button>
</div>
{/if}
+5 -1
View File
@@ -2,6 +2,10 @@
let { children } = $props();
</script>
<div class="flex min-h-0 flex-1 items-center justify-center overflow-auto">
<div class="mx-auto w-full max-w-md px-4 py-8 sm:py-12">
<div
class="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8 dark:border-slate-800 dark:bg-slate-900"
>
{@render children()}
</div>
</div>
@@ -0,0 +1,369 @@
<script lang="ts">
import { enhance } from '$app/forms';
import type { SubmitFunction } from '@sveltejs/kit';
import ProclamationBrowser from './ProclamationBrowser.svelte';
import TimeInput from '$lib/components/TimeInput.svelte';
import {
formatTime,
hmToMinutes,
isOvernight,
toHm,
validateBlockAgainstSchedule,
type BlockSibling
} from '$lib/schedule-utils';
type BrowserCategory = { id: number; name: string };
type BrowserGroup = { id: number; name: string };
type BrowserProclamation = {
id: string;
name: string;
categoryId: number;
groupId: number | null;
};
let {
open = false,
mode = 'create',
blockId,
day = null,
licenseKeyId = '',
initialStartMinutes = 9 * 60,
initialEndMinutes = 10 * 60,
initialProclamationIds = [],
initialDynamicAudio = true,
scheduleType = 'daily' as 'daily' | 'weekly' | null,
categories = [],
groups = [],
proclamations = [],
existingBlocks = [],
createAction = '?/createBlock',
updateAction = '?/updateBlock',
deleteAction = '?/deleteBlock',
onClose,
onSaved
}: {
open?: boolean;
mode?: 'create' | 'edit';
blockId?: string;
day?: string | null;
licenseKeyId?: string;
initialStartMinutes?: number;
initialEndMinutes?: number;
initialProclamationIds?: string[];
initialDynamicAudio?: boolean;
scheduleType?: 'daily' | 'weekly' | null;
categories?: BrowserCategory[];
groups?: BrowserGroup[];
proclamations?: BrowserProclamation[];
existingBlocks?: BlockSibling[];
createAction?: string;
updateAction?: string;
deleteAction?: string;
onClose?: () => void;
onSaved?: () => void;
} = $props();
let step = $state<'details' | 'browse'>('details');
// Seeded from the props in the effect below whenever the modal opens.
let startMinutes = $state(9 * 60);
let endMinutes = $state(10 * 60);
let startValid = $state(true);
let endValid = $state(true);
let selectedIds = $state<string[]>([]);
let dynamicAudio = $state(true);
let error = $state('');
function setStart(hm: string) {
const minutes = hmToMinutes(hm);
if (minutes !== null) startMinutes = minutes;
}
function setEnd(hm: string) {
const minutes = hmToMinutes(hm);
if (minutes !== null) endMinutes = minutes;
}
// Re-seed the fields whenever the modal is (re)opened.
$effect(() => {
if (!open) return;
startMinutes = initialStartMinutes;
endMinutes = initialEndMinutes;
startValid = true;
endValid = true;
selectedIds = [...initialProclamationIds];
dynamicAudio = initialDynamicAudio;
error = '';
step = 'details';
});
const overnight = $derived(
startMinutes !== endMinutes && isOvernight({ startMinutes, endMinutes })
);
const siblings = $derived(existingBlocks.filter((block) => block.id !== blockId));
const nameById = $derived(new Map(proclamations.map((p) => [p.id, p.name])));
const selectedTitles = $derived(
selectedIds
.map((id) => nameById.get(id))
.filter((name): name is string => typeof name === 'string')
);
function validate(): string | null {
if (!startValid || !endValid) return 'Enter a valid start and end time.';
if (scheduleType === null) return 'Set a schedule type first.';
const result = validateBlockAgainstSchedule(
scheduleType,
{ day, startMinutes, endMinutes, proclamationIds: selectedIds },
siblings
);
return result.ok ? null : result.error;
}
const handleSubmit: SubmitFunction = ({ formData, cancel }) => {
const intent = formData.get('intent');
if (intent !== 'delete') {
const validationError = validate();
if (validationError) {
error = validationError;
cancel();
return;
}
}
error = '';
return async ({ result, update }) => {
if (result.type === 'failure') {
const data = result.data as { error?: string } | undefined;
error = data?.error ?? 'Unable to save block.';
await update({ reset: false });
return;
}
if (result.type === 'error') {
error = 'Unable to save block.';
return;
}
await update();
onSaved?.();
};
};
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') onClose?.();
}
</script>
{#if open}
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/50 p-4"
onkeydown={handleKeydown}
role="presentation"
>
<button
type="button"
aria-label="Close dialog"
class="absolute inset-0 h-full w-full cursor-default"
onclick={() => onClose?.()}
></button>
<div
role="dialog"
aria-modal="true"
aria-label={mode === 'edit' ? 'Edit block' : 'Create block'}
class="relative z-10 flex max-h-[90vh] w-full max-w-3xl flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-xl dark:border-slate-800 dark:bg-slate-900"
>
<div
class="flex shrink-0 items-center justify-between gap-3 border-b border-slate-200 px-6 py-4 dark:border-slate-800"
>
<h2 class="text-lg font-bold text-slate-900 dark:text-slate-100">
{step === 'browse'
? 'Select Proclamations'
: mode === 'edit'
? 'Edit Block'
: 'Create Block'}
</h2>
<button
type="button"
onclick={() => onClose?.()}
aria-label="Close"
class="rounded-md p-1.5 text-2xl leading-none text-slate-400 transition hover:bg-slate-100 hover:text-red-600 dark:text-slate-500 dark:hover:bg-slate-800"
>
&times;
</button>
</div>
<form
method="POST"
action={mode === 'edit' ? updateAction : createAction}
use:enhance={handleSubmit}
class="flex min-h-0 flex-1 flex-col"
>
{#if mode === 'edit' && blockId}
<input type="hidden" name="id" value={blockId} />
{/if}
<input type="hidden" name="licenseKeyId" value={licenseKeyId} />
{#if day}
<input type="hidden" name="day" value={day} />
{/if}
<input type="hidden" name="startMinutes" value={startMinutes} />
<input type="hidden" name="endMinutes" value={endMinutes} />
<input type="hidden" name="proclamationIds" value={JSON.stringify(selectedIds)} />
<input type="hidden" name="dynamicAudio" value={dynamicAudio ? 'true' : 'false'} />
<div class="min-h-0 flex-1 overflow-y-auto px-6 py-5">
{#if error}
<div
class="mb-4 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600 dark:border-red-900 dark:bg-red-950/50 dark:text-red-400"
role="alert"
>
{error}
</div>
{/if}
{#if step === 'details'}
<div class="grid gap-6 md:grid-cols-[minmax(0,1fr)_16rem]">
<div class="flex flex-col gap-4">
<TimeInput
label="Start time"
value={toHm(startMinutes)}
onchange={setStart}
onvalidity={(valid) => (startValid = valid)}
/>
<TimeInput
label="End time"
value={toHm(endMinutes)}
onchange={setEnd}
onvalidity={(valid) => (endValid = valid)}
/>
<div>
<div class="flex items-center justify-between gap-2">
<span class="text-sm font-medium text-slate-700 dark:text-slate-300">
Proclamations ({selectedTitles.length})
</span>
<button
type="button"
onclick={() => (step = 'browse')}
class="rounded-md border border-indigo-200 bg-indigo-50 px-3 py-1.5 text-sm font-medium text-indigo-700 transition hover:bg-indigo-100 dark:border-indigo-900 dark:bg-indigo-950/50 dark:text-indigo-300"
>
Browse proclamations
</button>
</div>
</div>
<div class="flex items-center gap-2">
<label
class="flex items-center gap-2 text-sm font-medium text-slate-700 dark:text-slate-300"
>
<input
type="checkbox"
checked={dynamicAudio}
onchange={(e) => (dynamicAudio = e.currentTarget.checked)}
class="h-4 w-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500 dark:border-slate-600"
/>
Enable Dynamic Audio
</label>
<span class="group relative inline-flex">
<button
type="button"
aria-label="What is Dynamic Audio?"
aria-describedby="dynamic-audio-help"
class="flex h-5 w-5 items-center justify-center rounded-full border border-slate-300 text-xs font-semibold text-slate-500 transition hover:border-indigo-400 hover:text-indigo-600 focus:border-indigo-500 focus:outline-none dark:border-slate-600 dark:text-slate-400"
>
?
</button>
<span
id="dynamic-audio-help"
role="tooltip"
class="pointer-events-none absolute bottom-full left-1/2 z-20 mb-2 w-64 -translate-x-1/2 rounded-md bg-slate-900 px-3 py-2 text-xs font-normal text-white opacity-0 shadow-lg transition-opacity group-focus-within:opacity-100 group-hover:opacity-100 dark:bg-slate-700"
>
Dynamic audio will be enabled for this block, while this block is playing.
This will override any audio selections you have made on your device while
this block is active.
</span>
</span>
</div>
</div>
<div
class="rounded-lg border border-slate-200 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-950"
>
<h3 class="text-sm font-semibold text-slate-800 dark:text-slate-200">Review</h3>
<p class="mt-2 text-sm text-slate-600 dark:text-slate-400">
{formatTime(startMinutes)} – {formatTime(endMinutes)}{overnight
? ' (ends next day)'
: ''}
</p>
{#if selectedTitles.length === 0}
<p class="mt-2 text-sm text-slate-500 dark:text-slate-400">
No proclamations selected.
</p>
{:else}
<ul class="mt-2 space-y-1">
{#each selectedTitles as title (title)}
<li class="text-sm text-slate-700 dark:text-slate-300">{title}</li>
{/each}
</ul>
{/if}
</div>
</div>
{:else}
<ProclamationBrowser
{categories}
{groups}
{proclamations}
{selectedIds}
onChange={(ids) => (selectedIds = ids)}
/>
{/if}
</div>
<div
class="flex shrink-0 items-center justify-between gap-3 border-t border-slate-200 px-6 py-4 dark:border-slate-800"
>
<div class="flex items-center gap-3">
{#if step === 'details' && mode === 'edit' && blockId}
<button
type="submit"
name="intent"
value="delete"
formaction={deleteAction}
class="rounded-md bg-red-50 px-3 py-1.5 text-sm font-medium text-red-600 ring-1 ring-red-200 transition ring-inset hover:bg-red-600 hover:text-white dark:bg-red-950/50 dark:text-red-400 dark:ring-red-900"
>
Delete
</button>
{/if}
<button
type="button"
onclick={() => onClose?.()}
class="rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-800"
>
Cancel
</button>
</div>
<div class="flex items-center gap-3">
{#if step === 'browse'}
<button
type="button"
onclick={() => (step = 'details')}
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-indigo-700"
>
Accept
</button>
{:else}
<button
type="submit"
name="intent"
value="save"
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-indigo-700"
>
{mode === 'edit' ? 'Save Block' : 'Create Block'}
</button>
{/if}
</div>
</div>
</form>
</div>
</div>
{/if}
@@ -0,0 +1,198 @@
import { page } from 'vitest/browser';
import { describe, it, expect, vi } from 'vitest';
import { render } from 'vitest-browser-svelte';
const { submitRef } = vi.hoisted(() => ({
submitRef: {
current: null as unknown as (opts: { formData: FormData; cancel?: () => void }) => unknown
}
}));
vi.mock('$app/forms', () => ({
enhance: (_node: unknown, submit: unknown) => {
submitRef.current = submit as typeof submitRef.current;
return () => {};
}
}));
import BlockModal from './BlockModal.svelte';
const categories = [{ id: 1, name: 'Physical' }];
const groups = [{ id: 1, name: 'Prayers' }];
const proclamations = [
{ id: 'p1', name: 'Peace', categoryId: 1, groupId: 1 },
{ id: 'p2', name: 'Strength', categoryId: 1, groupId: 1 }
];
function mount(overrides: Record<string, unknown> = {}) {
return render(BlockModal, {
open: true,
mode: 'create',
categories,
groups,
proclamations,
existingBlocks: [],
initialStartMinutes: 6 * 60,
initialEndMinutes: 8 * 60,
initialProclamationIds: ['p1'],
onClose: () => {},
onSaved: () => {},
...overrides
});
}
function submit() {
const form = document.querySelector('form') as HTMLFormElement;
return submitRef.current({ formData: new FormData(form), cancel: () => {} });
}
async function typeTime(name: string, text: string) {
const input = (await page.getByRole('combobox', { name }).element()) as HTMLInputElement;
input.value = text;
input.dispatchEvent(new Event('input', { bubbles: true }));
}
describe('BlockModal', () => {
it('uses a 12-hour TimeInput for start and end', async () => {
mount();
await expect.element(page.getByRole('combobox', { name: 'Start time' })).toBeInTheDocument();
await expect.element(page.getByRole('combobox', { name: 'End time' })).toBeInTheDocument();
});
it('pre-fills the fields from the initial times', async () => {
mount({ initialStartMinutes: 7 * 60 + 30, initialEndMinutes: 9 * 60 + 15 });
await expect.element(page.getByRole('combobox', { name: 'Start time' })).toHaveValue('7:30 AM');
await expect.element(page.getByRole('combobox', { name: 'End time' })).toHaveValue('9:15 AM');
});
it('shows a 12-hour clock with AM/PM', async () => {
mount({ initialStartMinutes: 21 * 60, initialEndMinutes: 0 });
await expect.element(page.getByRole('combobox', { name: 'Start time' })).toHaveValue('9:00 PM');
await expect.element(page.getByRole('combobox', { name: 'End time' })).toHaveValue('12:00 AM');
});
it('recomputes the time from typed input', async () => {
mount({ initialStartMinutes: 9 * 60, initialEndMinutes: 10 * 60 });
await typeTime('End time', '10:00 PM');
await expect.element(page.getByText('9:00 AM – 10:00 PM')).toBeInTheDocument();
});
it('shows validation feedback for unparseable input', async () => {
mount();
await typeTime('Start time', '25:00');
await expect.element(page.getByRole('alert')).toBeInTheDocument();
});
it('shows the review times and selected proclamation titles', async () => {
mount();
await expect.element(page.getByText('6:00 AM – 8:00 AM')).toBeInTheDocument();
await expect.element(page.getByText('Peace')).toBeInTheDocument();
});
it('marks an overnight block in the review', async () => {
mount({ initialStartMinutes: 21 * 60, initialEndMinutes: 9 * 60 });
await expect.element(page.getByText('9:00 PM – 9:00 AM (ends next day)')).toBeInTheDocument();
});
it('rejects an overlapping time range with the specific error', async () => {
mount({
existingBlocks: [{ id: 'b1', startMinutes: 6 * 60, endMinutes: 8 * 60 }],
initialStartMinutes: 7 * 60,
initialEndMinutes: 9 * 60
});
submit();
await expect
.element(page.getByText(/This overlaps with your 6:00 AM–8:00 AM block\./))
.toBeInTheDocument();
});
it('requires at least one proclamation', async () => {
mount({ initialProclamationIds: [] });
submit();
await expect.element(page.getByText('Select at least one proclamation.')).toBeInTheDocument();
});
it('does not show an error for a valid selection', async () => {
mount();
const result = submit();
expect(typeof result).toBe('function');
await expect.element(page.getByRole('alert')).not.toBeInTheDocument();
});
it('shows Cancel with Accept in the browse step, and Accept returns to the details', async () => {
mount();
const browse = page.getByRole('button', { name: 'Browse proclamations' });
await expect.element(browse).toBeInTheDocument();
(browse.element() as HTMLElement).click();
await expect.element(page.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
const accept = page.getByRole('button', { name: 'Accept' });
await expect.element(accept).toBeInTheDocument();
await expect
.element(page.getByRole('button', { name: 'Create Block' }))
.not.toBeInTheDocument();
(accept.element() as HTMLElement).click();
await expect.element(page.getByRole('button', { name: 'Create Block' })).toBeInTheDocument();
await expect.element(page.getByRole('button', { name: 'Accept' })).not.toBeInTheDocument();
});
it('offers an Enable Dynamic Audio checkbox with a help tooltip', async () => {
mount();
const checkbox = page.getByRole('checkbox', { name: /Enable Dynamic Audio/ });
await expect.element(checkbox).toBeInTheDocument();
await expect
.element(page.getByText(/Dynamic audio will be enabled for this block/))
.toBeInTheDocument();
});
it('defaults dynamic audio on for new blocks', async () => {
mount();
const checkbox = page.getByRole('checkbox', { name: /Enable Dynamic Audio/ });
expect((checkbox.element() as HTMLInputElement).checked).toBe(true);
});
it('pre-checks dynamic audio when editing a block that has it enabled', async () => {
mount({ initialDynamicAudio: true });
const checkbox = page.getByRole('checkbox', { name: /Enable Dynamic Audio/ });
expect((checkbox.element() as HTMLInputElement).checked).toBe(true);
});
it('leaves dynamic audio off when editing a block that has it disabled', async () => {
mount({ initialDynamicAudio: false });
const checkbox = page.getByRole('checkbox', { name: /Enable Dynamic Audio/ });
expect((checkbox.element() as HTMLInputElement).checked).toBe(false);
});
it('toggles the dynamic audio flag in the submitted payload', async () => {
mount();
const checkbox = page.getByRole('checkbox', { name: /Enable Dynamic Audio/ });
await expect.element(checkbox).toBeInTheDocument();
(checkbox.element() as HTMLInputElement).click();
const hidden = document.querySelector('input[name="dynamicAudio"]') as HTMLInputElement;
await expect.poll(() => hidden.value).toBe('false');
});
});
@@ -0,0 +1,211 @@
<script lang="ts">
type BrowserCategory = { id: number; name: string };
type BrowserGroup = { id: number; name: string };
type BrowserProclamation = {
id: string;
name: string;
categoryId: number;
groupId: number | null;
};
let {
categories = [],
groups = [],
proclamations = [],
selectedIds = [],
onChange
}: {
categories: BrowserCategory[];
groups: BrowserGroup[];
proclamations: BrowserProclamation[];
selectedIds: string[];
onChange?: (ids: string[]) => void;
} = $props();
// Categories start collapsed. Groups are pre-expanded, so we track the
// smaller set of groups the user has explicitly collapsed.
let openCategoryIds = $state<number[]>([]);
let closedGroupIds = $state<number[]>([]);
const selectedSet = $derived(new Set(selectedIds));
const nameById = $derived(new Map(proclamations.map((p) => [p.id, p.name])));
const selectedTitles = $derived(
selectedIds
.map((id) => nameById.get(id))
.filter((name): name is string => typeof name === 'string')
);
type GroupView = { group: BrowserGroup; items: BrowserProclamation[] };
type CategoryView = {
category: BrowserCategory;
groups: GroupView[];
ungrouped: BrowserProclamation[];
};
const view = $derived.by<CategoryView[]>(() =>
categories.map((category) => {
const inCategory = proclamations.filter((p) => p.categoryId === category.id);
const groupViews = groups
.map((group) => ({
group,
items: inCategory.filter((p) => p.groupId === group.id)
}))
.filter((groupView) => groupView.items.length > 0);
const ungrouped = inCategory.filter((p) => p.groupId === null || p.groupId === undefined);
return { category, groups: groupViews, ungrouped };
})
);
function toggleCategory(id: number) {
openCategoryIds = openCategoryIds.includes(id)
? openCategoryIds.filter((value) => value !== id)
: [...openCategoryIds, id];
}
function toggleGroup(id: number) {
closedGroupIds = closedGroupIds.includes(id)
? closedGroupIds.filter((value) => value !== id)
: [...closedGroupIds, id];
}
function toggleProclamation(id: string) {
const next = selectedSet.has(id)
? selectedIds.filter((value) => value !== id)
: [...selectedIds, id];
onChange?.(next);
}
</script>
<div class="grid gap-4 md:grid-cols-[minmax(0,1fr)_16rem]">
<div
class="max-h-96 overflow-y-auto rounded-lg border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900"
>
{#if categories.length === 0}
<p class="p-4 text-sm text-slate-500 dark:text-slate-400">No categories available.</p>
{:else}
<ul class="divide-y divide-slate-100 dark:divide-slate-800">
{#each view as categoryView (categoryView.category.id)}
{@const isCategoryOpen = openCategoryIds.includes(categoryView.category.id)}
<li>
<button
type="button"
aria-expanded={isCategoryOpen}
onclick={() => toggleCategory(categoryView.category.id)}
class="flex w-full items-center justify-between gap-2 px-4 py-3 text-left text-sm font-semibold text-slate-800 transition hover:bg-slate-50 dark:text-slate-200 dark:hover:bg-slate-800"
>
<span>{categoryView.category.name}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="shrink-0 transition-transform {isCategoryOpen ? 'rotate-90' : ''}"
aria-hidden="true"><path d="m9 18 6-6-6-6" /></svg
>
</button>
{#if isCategoryOpen}
<div class="px-4 pb-3">
{#if categoryView.groups.length === 0 && categoryView.ungrouped.length === 0}
<p class="py-2 text-sm text-slate-500 dark:text-slate-400">
No proclamations in this category.
</p>
{/if}
{#each categoryView.groups as groupView (groupView.group.id)}
{@const isGroupOpen = !closedGroupIds.includes(groupView.group.id)}
<div class="mt-2">
<button
type="button"
aria-expanded={isGroupOpen}
onclick={() => toggleGroup(groupView.group.id)}
class="flex w-full items-center justify-between gap-2 rounded-md px-2 py-1.5 text-left text-xs font-semibold tracking-wide text-slate-500 uppercase transition hover:bg-slate-50 dark:text-slate-400 dark:hover:bg-slate-800"
>
<span>{groupView.group.name}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="shrink-0 transition-transform {isGroupOpen ? 'rotate-180' : ''}"
aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg
>
</button>
{#if isGroupOpen}
<ul class="mt-1 space-y-0.5">
{#each groupView.items as proclamation (proclamation.id)}
<li>
<label
class="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm text-slate-700 transition hover:bg-indigo-50 dark:text-slate-300 dark:hover:bg-indigo-950"
>
<input
type="checkbox"
checked={selectedSet.has(proclamation.id)}
onchange={() => toggleProclamation(proclamation.id)}
class="h-4 w-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500 dark:border-slate-600"
/>
<span>{proclamation.name}</span>
</label>
</li>
{/each}
</ul>
{/if}
</div>
{/each}
{#if categoryView.ungrouped.length > 0}
<div class="mt-3 border-t border-slate-100 pt-2 dark:border-slate-800">
<ul class="space-y-0.5">
{#each categoryView.ungrouped as proclamation (proclamation.id)}
<li>
<label
class="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm text-slate-700 transition hover:bg-indigo-50 dark:text-slate-300 dark:hover:bg-indigo-950"
>
<input
type="checkbox"
checked={selectedSet.has(proclamation.id)}
onchange={() => toggleProclamation(proclamation.id)}
class="h-4 w-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500 dark:border-slate-600"
/>
<span>{proclamation.name}</span>
</label>
</li>
{/each}
</ul>
</div>
{/if}
</div>
{/if}
</li>
{/each}
</ul>
{/if}
</div>
<div
class="rounded-lg border border-slate-200 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-950"
>
<h3 class="text-sm font-semibold text-slate-800 dark:text-slate-200">
Selected ({selectedTitles.length})
</h3>
{#if selectedTitles.length === 0}
<p class="mt-2 text-sm text-slate-500 dark:text-slate-400">No proclamations selected.</p>
{:else}
<ul class="mt-2 space-y-1">
{#each selectedTitles as title, index (selectedIds[index])}
<li class="text-sm text-slate-700 dark:text-slate-300">{title}</li>
{/each}
</ul>
{/if}
</div>
</div>
@@ -0,0 +1,107 @@
import { page } from 'vitest/browser';
import { describe, it, expect, vi } from 'vitest';
import { render } from 'vitest-browser-svelte';
import ProclamationBrowser from './ProclamationBrowser.svelte';
const categories = [
{ id: 1, name: 'Physical' },
{ id: 2, name: 'Mental' },
{ id: 3, name: 'Empty' }
];
const groups = [
{ id: 1, name: 'Prayers' },
{ id: 2, name: 'Scripture' }
];
const proclamations = [
{ id: 'p1', name: 'Healing', categoryId: 1, groupId: 1 },
{ id: 'p2', name: 'Strength', categoryId: 1, groupId: 1 },
{ id: 'p3', name: 'Focus', categoryId: 1, groupId: null },
{ id: 'p4', name: 'Calm', categoryId: 2, groupId: 2 }
];
function mount(overrides: Record<string, unknown> = {}) {
return render(ProclamationBrowser, {
categories,
groups,
proclamations,
selectedIds: [],
onChange: () => {},
...overrides
});
}
async function click(locator: { element: () => HTMLElement | SVGElement }) {
const el = locator.element() as HTMLElement;
el.click();
}
describe('ProclamationBrowser', () => {
it('collapses categories by default', async () => {
mount();
await expect.element(page.getByRole('button', { name: 'Physical' })).toBeInTheDocument();
await expect.element(page.getByRole('checkbox', { name: 'Healing' })).not.toBeInTheDocument();
});
it('reveals pre-expanded groups and their proclamations when a category opens', async () => {
mount();
await click(page.getByRole('button', { name: 'Physical' }));
await expect.element(page.getByText('Prayers')).toBeInTheDocument();
await expect.element(page.getByRole('checkbox', { name: 'Healing' })).toBeInTheDocument();
await expect.element(page.getByRole('checkbox', { name: 'Strength' })).toBeInTheDocument();
});
it('hides groups that have no proclamations in the category', async () => {
mount();
await click(page.getByRole('button', { name: 'Physical' }));
await expect.element(page.getByText('Scripture')).not.toBeInTheDocument();
});
it('lists ungrouped proclamations below the groups', async () => {
mount();
await click(page.getByRole('button', { name: 'Physical' }));
await expect.element(page.getByRole('checkbox', { name: 'Focus' })).toBeInTheDocument();
});
it('reports toggled selections through onChange', async () => {
const onChange = vi.fn();
mount({ selectedIds: ['p1'], onChange });
await click(page.getByRole('button', { name: 'Physical' }));
await click(page.getByRole('checkbox', { name: 'Strength' }));
expect(onChange).toHaveBeenCalledWith(['p1', 'p2']);
await click(page.getByRole('checkbox', { name: 'Healing' }));
expect(onChange).toHaveBeenCalledWith([]);
});
it('shows selected titles in the summary and keeps selection across collapse', async () => {
mount({ selectedIds: ['p1'] });
await expect.element(page.getByText('Selected (1)')).toBeInTheDocument();
await click(page.getByRole('button', { name: 'Physical' }));
const checkbox = (await page
.getByRole('checkbox', { name: 'Healing' })
.element()) as HTMLInputElement;
expect(checkbox.checked).toBe(true);
await click(page.getByRole('button', { name: 'Physical' }));
await expect.element(page.getByRole('checkbox', { name: 'Healing' })).not.toBeInTheDocument();
await click(page.getByRole('button', { name: 'Physical' }));
const reopened = (await page
.getByRole('checkbox', { name: 'Healing' })
.element()) as HTMLInputElement;
expect(reopened.checked).toBe(true);
});
});
@@ -0,0 +1,287 @@
<script lang="ts">
import {
MINUTES_PER_DAY,
SNAP_MINUTES,
blockSegments,
formatTime,
isOvernight,
snapMinutes
} from '$lib/schedule-utils';
type ScheduleBlockView = {
id: string;
day?: string | null;
startMinutes: number;
endMinutes: number;
dynamicAudio: boolean;
proclamations: { id: string; name: string }[];
};
type OpenBlock = {
id: string;
day: string | null;
startMinutes: number;
endMinutes: number;
dynamicAudio: boolean;
proclamations: { id: string; name: string }[];
};
let {
days,
blocks = [],
onCreate,
onEdit
}: {
days: { key: string; label: string; short?: string }[];
blocks?: ScheduleBlockView[];
onCreate?: (day: string, startMinutes: number, endMinutes: number) => void;
onEdit?: (block: OpenBlock) => void;
} = $props();
const hourMarks = Array.from({ length: 24 }, (_, i) => i);
const defaultDuration = 60;
// Which grid the pointer is currently over, so the live preview shows up in
// the right day column (Google-Calendar style).
let drag = $state<{
day: string;
anchor: number;
current: number;
pointerId: number;
} | null>(null);
function dayLabel(key: string): string {
return days.find((d) => d.key === key)?.label ?? key;
}
function minutesFromEvent(event: PointerEvent, column: HTMLElement): number {
const rect = column.getBoundingClientRect();
// Guard against an unmeasured/zero-height column (e.g. before layout) so
// a tap still yields a valid, finite start minute instead of NaN.
if (!rect.height) return 0;
const ratio = (event.clientY - rect.top) / rect.height;
return Math.max(0, Math.min(MINUTES_PER_DAY, snapMinutes(ratio * MINUTES_PER_DAY)));
}
// Pointer capture happens on the column element, so moves/ups land there even
// when the pointer leaves the small cell.
function startDrag(event: PointerEvent, day: string) {
if (event.pointerType === 'mouse' && event.button !== 0) return;
const column = event.currentTarget as HTMLElement;
const minutes = minutesFromEvent(event, column);
drag = { day, anchor: minutes, current: minutes, pointerId: event.pointerId };
column.setPointerCapture(event.pointerId);
event.preventDefault();
}
function moveDrag(event: PointerEvent) {
if (!drag || event.pointerId !== drag.pointerId) return;
const column = event.currentTarget as HTMLElement;
drag = { ...drag, current: minutesFromEvent(event, column) };
}
function endDrag(event: PointerEvent) {
if (!drag || event.pointerId !== drag.pointerId) return;
const { day, anchor, current } = drag;
const column = event.currentTarget as HTMLElement;
column.releasePointerCapture?.(event.pointerId);
drag = null;
const start = Math.min(anchor, current);
const end = Math.max(anchor, current);
if (end - start >= SNAP_MINUTES) {
onCreate?.(day, start, end);
return;
}
// A plain click creates a default-length block starting at that slot.
const clickStart = Math.min(start, MINUTES_PER_DAY - defaultDuration);
const clickEnd = Math.min(MINUTES_PER_DAY, clickStart + defaultDuration);
onCreate?.(day, Math.max(0, clickStart), clickEnd);
}
const preview = $derived.by(() => {
if (!drag) return null;
const start = Math.min(drag.anchor, drag.current);
const end = Math.max(drag.anchor, drag.current);
return {
day: drag.day,
top: (start / MINUTES_PER_DAY) * 100,
height: ((end - start) / MINUTES_PER_DAY) * 100
};
});
function blocksForDay(day: string): ScheduleBlockView[] {
// Daily schedules use a single "daily" column while their Blocks carry
// day=null; weekly columns match the `mon`..`sun` keys directly.
return blocks.filter((b) => (day === 'daily' ? b.day == null : b.day === day));
}
// The column before each day, wrapping across the schedule (mon's previous
// day is sun; the single daily column is its own previous day). Used to spill
// an overnight block's post-midnight tail into the next day's column.
const previousDayKey: Record<string, string> = $derived.by(() => {
const map: Record<string, string> = {};
days.forEach((day, index) => {
map[day.key] = days[(index - 1 + days.length) % days.length].key;
});
return map;
});
type DayView = {
block: ScheduleBlockView;
// A same-day block, the pre-midnight start of an overnight block, or the
// post-midnight tail of the previous day's overnight block.
part: 'whole' | 'head' | 'tail';
top: number;
height: number;
};
function toView(
block: ScheduleBlockView,
part: DayView['part'],
[start, end]: [number, number]
): DayView {
return {
block,
part,
top: (start / MINUTES_PER_DAY) * 100,
height: ((end - start) / MINUTES_PER_DAY) * 100
};
}
// A column shows its own blocks plus the tails of the previous day's
// overnight blocks. An overnight block contributes only its pre-midnight
// head to its start day; its tail is drawn at the top of the next day
// (weekly sun 7PM→1AM spills into mon). For daily, the previous day is the
// same single column, so both parts render together as before.
function viewsForDay(day: string): DayView[] {
const views: DayView[] = [];
for (const block of blocksForDay(day)) {
const head = blockSegments(block)[0];
if (!head || head[1] <= head[0]) continue;
views.push(toView(block, isOvernight(block) ? 'head' : 'whole', head));
}
const previous = previousDayKey[day];
if (previous) {
for (const block of blocksForDay(previous)) {
if (!isOvernight(block)) continue;
const tail = blockSegments(block)[1];
if (!tail || tail[1] <= tail[0]) continue;
views.push(toView(block, 'tail', tail));
}
}
return views;
}
</script>
<!--
Schedule as a Google-Calendar-style grid: day columns side by side with a
shared time gutter. Scrolls horizontally on narrow screens (the gutter stays
pinned); tap/click or drag an empty slot to create a block at that time, tap a
block to edit it.
-->
<div
class="isolate overflow-hidden rounded-lg border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900"
>
<div class="flex overflow-x-auto" style="overscroll-behavior-x: contain;">
<!-- Time gutter (pinned while the day columns scroll horizontally). -->
<div
class="sticky left-0 z-30 w-20 shrink-0 border-r border-slate-200 bg-white sm:w-24 dark:border-slate-800 dark:bg-slate-900"
>
<!-- Spacer aligning with the day header row. -->
<div class="h-12 border-b border-slate-200 dark:border-slate-800"></div>
<div class="relative h-[720px]" aria-hidden="true">
{#each hourMarks as hour (hour)}
<div
class="absolute right-2 -translate-y-1/2 text-xs whitespace-nowrap text-slate-400 tabular-nums dark:text-slate-500"
style={`top:${(hour / 24) * 100}%`}
>
{formatTime(hour * 60)}
</div>
{/each}
</div>
</div>
<!-- Day columns share the available width equally so a weekly view fits
without scrolling on desktop. Each keeps a minimum width, so when the
seven days cannot fit (phones) the row scrolls horizontally. -->
{#each days as day (day.key)}
<div
class="min-w-24 flex-1 basis-0 border-r border-slate-200 last:border-r-0 dark:border-slate-800"
>
<div
class="flex h-12 items-center justify-center border-b border-slate-200 px-1 text-xs font-semibold text-slate-700 sm:text-sm dark:border-slate-800 dark:text-slate-200"
>
{#if day.short}
<span class="sm:hidden">{day.short}</span>
<span class="hidden sm:inline">{day.label}</span>
{:else}
{day.label}
{/if}
</div>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="relative h-[720px] cursor-pointer select-none"
style="touch-action: pan-x pan-y;"
aria-label={`Schedule grid ${dayLabel(day.key)}`}
onpointerdown={(e) => startDrag(e, day.key)}
onpointermove={moveDrag}
onpointerup={endDrag}
onpointercancel={(e) => {
if (drag && e.pointerId === drag.pointerId) drag = null;
}}
>
{#each hourMarks as hour (hour)}
<div
class="pointer-events-none absolute inset-x-0 border-t border-slate-100 dark:border-slate-800"
style={`top:${(hour / 24) * 100}%`}
></div>
{/each}
{#if preview && preview.day === day.key}
<div
class="pointer-events-none absolute inset-x-1 z-10 rounded-md bg-indigo-200/70 dark:bg-indigo-700/50"
style={`top:${preview.top}%;height:${preview.height}%`}
></div>
{/if}
{#each viewsForDay(day.key) as view (`${view.block.id}-${view.part}`)}
<button
type="button"
onpointerdown={(event) => event.stopPropagation()}
onclick={(event) => {
event.stopPropagation();
onEdit?.({ ...view.block, day: view.block.day ?? null });
}}
aria-label={`Block ${formatTime(view.block.startMinutes)} to ${formatTime(view.block.endMinutes)}`}
class="absolute inset-x-0.5 z-20 overflow-hidden rounded-md border border-indigo-300 bg-indigo-50 px-1.5 py-1 text-left text-xs text-indigo-900 shadow-sm transition hover:bg-indigo-100 dark:border-indigo-800 dark:bg-indigo-950/70 dark:text-indigo-200 dark:hover:bg-indigo-900"
style={`top:${view.top}%;height:${view.height}%`}
>
{#if view.part === 'tail'}
<span class="block truncate font-semibold tabular-nums">
{formatTime(0)}–{formatTime(view.block.endMinutes)}
</span>
<span class="block truncate text-[11px] opacity-80">↳ continues</span>
{:else}
<span class="block truncate font-semibold tabular-nums">
{formatTime(view.block.startMinutes)}–{formatTime(view.block.endMinutes)}
</span>
{#if view.part === 'head'}
<span class="block truncate text-[11px] opacity-80">ends next day</span>
{/if}
{/if}
<span class="block truncate">
{view.block.proclamations.map((p) => p.name).join(', ') || 'No proclamations'}
</span>
</button>
{/each}
</div>
</div>
{/each}
</div>
</div>
@@ -0,0 +1,172 @@
import { page } from 'vitest/browser';
import { describe, it, expect, vi } from 'vitest';
import { render } from 'vitest-browser-svelte';
import WeekSchedule from './WeekSchedule.svelte';
const WEEK_DAYS = [
{ key: 'mon', label: 'Monday' },
{ key: 'tue', label: 'Tuesday' },
{ key: 'wed', label: 'Wednesday' },
{ key: 'thu', label: 'Thursday' },
{ key: 'fri', label: 'Friday' },
{ key: 'sat', label: 'Saturday' },
{ key: 'sun', label: 'Sunday' }
];
const block = (overrides: Record<string, unknown> = {}) => ({
id: 'b1',
day: 'mon',
startMinutes: 6 * 60,
endMinutes: 8 * 60,
dynamicAudio: true,
proclamations: [{ id: 'p1', name: 'Peace' }],
...overrides
});
function mount(props: Record<string, unknown> = {}) {
return render(WeekSchedule, { days: WEEK_DAYS, blocks: [], ...props });
}
// Click at a fraction of the way down a day column.
async function clickColumn(label: string, fraction: number) {
const column = document.querySelector(
`[aria-label="Schedule grid ${label}"]`
) as HTMLElement | null;
if (!column) throw new Error(`no column ${label}`);
const rect = column.getBoundingClientRect();
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height * fraction;
column.dispatchEvent(
new PointerEvent('pointerdown', { clientX: x, clientY: y, bubbles: true, pointerId: 1 })
);
column.dispatchEvent(
new PointerEvent('pointerup', { clientX: x, clientY: y, bubbles: true, pointerId: 1 })
);
}
describe('WeekSchedule', () => {
it('renders all seven day columns side by side', async () => {
mount();
for (const day of WEEK_DAYS) {
await expect.element(page.getByText(day.label)).toBeInTheDocument();
}
});
it('renders a single column for a daily schedule', async () => {
mount({ days: [{ key: 'daily', label: 'Every day' }] });
await expect.element(page.getByText('Every day')).toBeInTheDocument();
expect(document.querySelectorAll('[aria-label^="Schedule grid"]')).toHaveLength(1);
});
it('is horizontally scrollable with a pinned time gutter (mobile-friendly)', async () => {
mount();
// The day columns live in a horizontally scrollable row.
const scroller = document.querySelector('.overflow-x-auto') as HTMLElement | null;
expect(scroller).not.toBeNull();
// The time gutter is sticky so it stays visible while days scroll.
const gutter = scroller?.querySelector('[class*="sticky"][class*="left-0"]');
expect(gutter).not.toBeNull();
// Each day column has a minimum basis width so it stays legible on a phone.
const columns = scroller?.querySelectorAll('[aria-label^="Schedule grid"]') ?? [];
expect(columns.length).toBe(7);
});
it('shows short day labels on small screens', async () => {
mount({
days: [
{ key: 'mon', label: 'Monday', short: 'Mon' },
{ key: 'tue', label: 'Tuesday', short: 'Tue' }
]
});
// Both the short (small-screen) and long (large-screen) labels exist in
// the DOM; CSS decides which is visible at a given width.
const text = document.body.textContent ?? '';
expect(text).toContain('Mon');
expect(text).toContain('Monday');
});
it('shows blocks as events', async () => {
mount({ blocks: [block()] });
await expect.element(page.getByText('Peace')).toBeInTheDocument();
await expect.element(page.getByLabelText('Block 6:00 AM to 8:00 AM')).toBeInTheDocument();
});
it('renders daily blocks (day null) in the single daily column', async () => {
mount({
days: [{ key: 'daily', label: 'Every day' }],
blocks: [block({ day: null })]
});
await expect.element(page.getByText('Peace')).toBeInTheDocument();
await expect.element(page.getByLabelText('Block 6:00 AM to 8:00 AM')).toBeInTheDocument();
});
function column(label: string): HTMLElement {
return document.querySelector(`[aria-label="Schedule grid ${label}"]`) as HTMLElement;
}
it('spills an overnight weekly block into the next day column', async () => {
mount({ blocks: [block({ day: 'sun', startMinutes: 19 * 60, endMinutes: 60 })] });
// The start day keeps only the pre-midnight head.
const sunday = column('Sunday');
expect(sunday.textContent).toContain('7:00 PM–1:00 AM');
expect(sunday.textContent).toContain('ends next day');
expect(sunday.textContent).not.toContain('continues');
// The post-midnight tail is drawn on the following day.
const monday = column('Monday');
expect(monday.textContent).toContain('12:00 AM–1:00 AM');
expect(monday.textContent).toContain('↳ continues');
});
it('spills a Saturday overnight block into Sunday', async () => {
mount({ blocks: [block({ day: 'sat', startMinutes: 23 * 60, endMinutes: 30 })] });
const sunday = column('Sunday');
expect(sunday.textContent).toContain('12:00 AM–12:30 AM');
expect(sunday.textContent).toContain('↳ continues');
});
it('keeps both parts of a daily overnight block in its single column', async () => {
mount({
days: [{ key: 'daily', label: 'Every day' }],
blocks: [block({ day: null, startMinutes: 21 * 60, endMinutes: 9 * 60 })]
});
const daily = column('Every day');
expect(daily.textContent).toContain('9:00 PM–9:00 AM');
expect(daily.textContent).toContain('ends next day');
expect(daily.textContent).toContain('12:00 AM–9:00 AM');
expect(daily.textContent).toContain('↳ continues');
});
it('opens create on the clicked day', async () => {
const onCreate = vi.fn();
mount({ onCreate });
await clickColumn('Monday', 0.25);
expect(onCreate).toHaveBeenCalledTimes(1);
const [day, start, end] = onCreate.mock.calls[0] as [string, number, number];
expect(day).toBe('mon');
// The handler always yields a sensible minute range (a default-length
// block when the click distance is below the snap threshold).
expect(Number.isFinite(start)).toBe(true);
expect(Number.isFinite(end)).toBe(true);
expect(end - start).toBeGreaterThan(0);
});
it('opens edit when a block is clicked', async () => {
const onEdit = vi.fn();
mount({ blocks: [block()], onEdit });
await page.getByLabelText('Block 6:00 AM to 8:00 AM').click();
expect(onEdit).toHaveBeenCalledTimes(1);
expect(onEdit.mock.calls[0][0]).toMatchObject({ id: 'b1', day: 'mon' });
});
});
-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} />
+160 -6
View File
@@ -2,18 +2,172 @@ import pg from 'pg';
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '$lib/generated/prisma/client';
import { env } from '$env/dynamic/private';
import { getAuditContext } from '$lib/server/audit-context';
import { describeAction, entityIdFor, sanitizeAuditValue, shouldAudit } from '$lib/server/audit';
let _prisma: PrismaClient | undefined;
let _base: PrismaClient | undefined;
let _pool: pg.Pool | undefined;
function getBase(): { base: PrismaClient; pool: pg.Pool } {
if (!_base || !_pool) {
_pool = new pg.Pool({ connectionString: env.DATABASE_URL });
const adapter = new PrismaPg(_pool);
_base = new PrismaClient({ adapter });
}
return { base: _base, pool: _pool };
}
const roleNameCache = new Map<number, string>();
async function resolveRoleName(base: PrismaClient, roleId: number | null): Promise<string | null> {
if (roleId === null) return null;
const cached = roleNameCache.get(roleId);
if (cached) return cached;
try {
const role = await base.userRole.findUnique({
where: { id: roleId },
select: { name: true }
});
if (role?.name) {
roleNameCache.set(roleId, role.name);
return role.name;
}
} catch (e) {
console.error('Failed to resolve role name for audit log:', e);
}
return null;
}
type Delegate = {
findFirst?: (args: unknown) => Promise<unknown>;
findMany?: (args: unknown) => Promise<unknown>;
};
function delegateFor(base: PrismaClient, model: string): Delegate | undefined {
const key = model.charAt(0).toLowerCase() + model.slice(1);
return (base as unknown as Record<string, unknown>)[key] as Delegate | undefined;
}
async function readBefore(
base: PrismaClient,
model: string,
operation: string,
args: unknown
): Promise<unknown> {
if (operation.startsWith('create')) return null;
const delegate = delegateFor(base, model);
if (!delegate) return null;
const where = (args as { where?: unknown } | null)?.where;
try {
if (operation === 'updateMany' || operation === 'deleteMany') {
return delegate.findMany ? await delegate.findMany({ where, take: 50 }) : null;
}
return delegate.findFirst ? await delegate.findFirst({ where }) : null;
} catch {
return null;
}
}
async function writeAuditRow(
pool: pg.Pool,
base: PrismaClient,
entry: {
userId: string | null;
email: string | null;
roleId: number | null;
page: string;
action: string;
entity: string;
entityId: string | null;
before: unknown;
after: unknown;
ip: string | null;
}
): Promise<void> {
const roleName = await resolveRoleName(base, entry.roleId);
await pool.query(
`INSERT INTO "AuditLog"
("id", "createdAt", "userId", "email", "roleId", "roleName", "page", "action", "entity", "entityId", "before", "after", "ip")
VALUES ($1, now(), $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb, $11::jsonb, $12)`,
[
crypto.randomUUID(),
entry.userId,
entry.email,
entry.roleId,
roleName,
entry.page,
entry.action,
entry.entity,
entry.entityId,
entry.before === null ? null : JSON.stringify(entry.before),
entry.after === null ? null : JSON.stringify(entry.after),
entry.ip
]
);
}
function getPrisma(): PrismaClient {
if (!_prisma) {
const pool = new pg.Pool({
connectionString: env.DATABASE_URL
if (_prisma) return _prisma;
const { base, pool } = getBase();
const extended = base.$extends({
query: {
$allModels: {
async $allOperations({ model, operation, args, query }) {
if (!shouldAudit(model, operation)) {
return query(args);
}
const before = await readBefore(base, model, operation, args);
const result = await query(args);
try {
const ctx = getAuditContext();
const res = result as { id?: unknown; email?: string; roleId?: number | null } | null;
let userId = ctx?.userId ?? null;
let email = ctx?.email ?? null;
let roleId = ctx?.roleId ?? null;
// Self-signup: attribute the new user to itself since no session exists yet.
if (!userId && model === 'User' && operation === 'create' && res) {
userId = (res.id as string | undefined) ?? null;
email = res.email ?? null;
roleId = res.roleId ?? null;
}
const after =
operation === 'updateMany' || operation === 'deleteMany'
? { count: (result as { count?: number } | null)?.count ?? null }
: operation.startsWith('delete')
? null
: result;
await writeAuditRow(pool, base, {
userId,
email,
roleId,
page: ctx?.page ?? '/system',
action: describeAction(operation),
entity: model,
entityId: entityIdFor(args, result, before),
before: sanitizeAuditValue(before),
after: sanitizeAuditValue(after),
ip: ctx?.ip ?? null
});
} catch (e) {
console.error('Failed to write audit log:', e);
}
return result;
}
}
}
});
const adapter = new PrismaPg(pool);
_prisma = new PrismaClient({ adapter });
}
_prisma = extended as unknown as PrismaClient;
return _prisma;
}
+25 -10
View File
@@ -57,6 +57,11 @@ export type Verification = Prisma.VerificationModel
*
*/
export type Proclamation = Prisma.ProclamationModel
/**
* Model ProclamationGroup
*
*/
export type ProclamationGroup = Prisma.ProclamationGroupModel
/**
* Model UserProclamations
*
@@ -72,11 +77,6 @@ export type SolfeggioFrequency = Prisma.SolfeggioFrequencyModel
*
*/
export type ProclamationCategory = Prisma.ProclamationCategoryModel
/**
* Model ProclamationCategorySubscriptionLevel
*
*/
export type ProclamationCategorySubscriptionLevel = Prisma.ProclamationCategorySubscriptionLevelModel
/**
* Model LicenseKey
*
@@ -87,13 +87,28 @@ export type LicenseKey = Prisma.LicenseKeyModel
*
*/
export type ApiKey = Prisma.ApiKeyModel
/**
* Model SubscriptionLevel
*
*/
export type SubscriptionLevel = Prisma.SubscriptionLevelModel
/**
* Model ColorPalette
*
*/
export type ColorPalette = Prisma.ColorPaletteModel
/**
* Model AuditLog
*
*/
export type AuditLog = Prisma.AuditLogModel
/**
* Model Schedule
*
*/
export type Schedule = Prisma.ScheduleModel
/**
* Model Block
*
*/
export type Block = Prisma.BlockModel
/**
* Model BlockProclamations
*
*/
export type BlockProclamations = Prisma.BlockProclamationsModel
+25 -10
View File
@@ -81,6 +81,11 @@ export type Verification = Prisma.VerificationModel
*
*/
export type Proclamation = Prisma.ProclamationModel
/**
* Model ProclamationGroup
*
*/
export type ProclamationGroup = Prisma.ProclamationGroupModel
/**
* Model UserProclamations
*
@@ -96,11 +101,6 @@ export type SolfeggioFrequency = Prisma.SolfeggioFrequencyModel
*
*/
export type ProclamationCategory = Prisma.ProclamationCategoryModel
/**
* Model ProclamationCategorySubscriptionLevel
*
*/
export type ProclamationCategorySubscriptionLevel = Prisma.ProclamationCategorySubscriptionLevelModel
/**
* Model LicenseKey
*
@@ -111,13 +111,28 @@ export type LicenseKey = Prisma.LicenseKeyModel
*
*/
export type ApiKey = Prisma.ApiKeyModel
/**
* Model SubscriptionLevel
*
*/
export type SubscriptionLevel = Prisma.SubscriptionLevelModel
/**
* Model ColorPalette
*
*/
export type ColorPalette = Prisma.ColorPaletteModel
/**
* Model AuditLog
*
*/
export type AuditLog = Prisma.AuditLogModel
/**
* Model Schedule
*
*/
export type Schedule = Prisma.ScheduleModel
/**
* Model Block
*
*/
export type Block = Prisma.BlockModel
/**
* Model BlockProclamations
*
*/
export type BlockProclamations = Prisma.BlockProclamationsModel
@@ -253,6 +253,57 @@ export type JsonWithAggregatesFilterBase<$PrismaModel = never> = {
_max?: Prisma.NestedJsonFilter<$PrismaModel>
}
export type JsonNullableFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<JsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>,
Required<JsonNullableFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>
export type JsonNullableFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}
export type JsonNullableWithAggregatesFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>,
Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>
export type JsonNullableWithAggregatesFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedJsonNullableFilter<$PrismaModel>
_max?: Prisma.NestedJsonNullableFilter<$PrismaModel>
}
export type NestedStringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
@@ -478,4 +529,28 @@ export type NestedJsonFilterBase<$PrismaModel = never> = {
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}
export type NestedJsonNullableFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<NestedJsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>,
Required<NestedJsonNullableFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>
export type NestedJsonNullableFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}
File diff suppressed because one or more lines are too long
@@ -392,14 +392,17 @@ export const ModelName = {
UserRole: 'UserRole',
Verification: 'Verification',
Proclamation: 'Proclamation',
ProclamationGroup: 'ProclamationGroup',
UserProclamations: 'UserProclamations',
SolfeggioFrequency: 'SolfeggioFrequency',
ProclamationCategory: 'ProclamationCategory',
ProclamationCategorySubscriptionLevel: 'ProclamationCategorySubscriptionLevel',
LicenseKey: 'LicenseKey',
ApiKey: 'ApiKey',
SubscriptionLevel: 'SubscriptionLevel',
ColorPalette: 'ColorPalette'
ColorPalette: 'ColorPalette',
AuditLog: 'AuditLog',
Schedule: 'Schedule',
Block: 'Block',
BlockProclamations: 'BlockProclamations'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
@@ -415,7 +418,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
omit: GlobalOmitOptions
}
meta: {
modelProps: "user" | "profile" | "address" | "session" | "account" | "userRole" | "verification" | "proclamation" | "userProclamations" | "solfeggioFrequency" | "proclamationCategory" | "proclamationCategorySubscriptionLevel" | "licenseKey" | "apiKey" | "subscriptionLevel" | "colorPalette"
modelProps: "user" | "profile" | "address" | "session" | "account" | "userRole" | "verification" | "proclamation" | "proclamationGroup" | "userProclamations" | "solfeggioFrequency" | "proclamationCategory" | "licenseKey" | "apiKey" | "colorPalette" | "auditLog" | "schedule" | "block" | "blockProclamations"
txIsolationLevel: TransactionIsolationLevel
}
model: {
@@ -1011,6 +1014,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 +1310,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 +1458,6 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
}
}
}
SubscriptionLevel: {
payload: Prisma.$SubscriptionLevelPayload<ExtArgs>
fields: Prisma.SubscriptionLevelFieldRefs
operations: {
findUnique: {
args: Prisma.SubscriptionLevelFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload> | null
}
findUniqueOrThrow: {
args: Prisma.SubscriptionLevelFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>
}
findFirst: {
args: Prisma.SubscriptionLevelFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload> | null
}
findFirstOrThrow: {
args: Prisma.SubscriptionLevelFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>
}
findMany: {
args: Prisma.SubscriptionLevelFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>[]
}
create: {
args: Prisma.SubscriptionLevelCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>
}
createMany: {
args: Prisma.SubscriptionLevelCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.SubscriptionLevelCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>[]
}
delete: {
args: Prisma.SubscriptionLevelDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>
}
update: {
args: Prisma.SubscriptionLevelUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>
}
deleteMany: {
args: Prisma.SubscriptionLevelDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.SubscriptionLevelUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.SubscriptionLevelUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>[]
}
upsert: {
args: Prisma.SubscriptionLevelUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SubscriptionLevelPayload>
}
aggregate: {
args: Prisma.SubscriptionLevelAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateSubscriptionLevel>
}
groupBy: {
args: Prisma.SubscriptionLevelGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.SubscriptionLevelGroupByOutputType>[]
}
count: {
args: Prisma.SubscriptionLevelCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.SubscriptionLevelCountAggregateOutputType> | number
}
}
}
ColorPalette: {
payload: Prisma.$ColorPalettePayload<ExtArgs>
fields: Prisma.ColorPaletteFieldRefs
@@ -1603,6 +1532,302 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
}
}
}
AuditLog: {
payload: Prisma.$AuditLogPayload<ExtArgs>
fields: Prisma.AuditLogFieldRefs
operations: {
findUnique: {
args: Prisma.AuditLogFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AuditLogPayload> | null
}
findUniqueOrThrow: {
args: Prisma.AuditLogFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AuditLogPayload>
}
findFirst: {
args: Prisma.AuditLogFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AuditLogPayload> | null
}
findFirstOrThrow: {
args: Prisma.AuditLogFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AuditLogPayload>
}
findMany: {
args: Prisma.AuditLogFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AuditLogPayload>[]
}
create: {
args: Prisma.AuditLogCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AuditLogPayload>
}
createMany: {
args: Prisma.AuditLogCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.AuditLogCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AuditLogPayload>[]
}
delete: {
args: Prisma.AuditLogDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AuditLogPayload>
}
update: {
args: Prisma.AuditLogUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AuditLogPayload>
}
deleteMany: {
args: Prisma.AuditLogDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.AuditLogUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.AuditLogUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AuditLogPayload>[]
}
upsert: {
args: Prisma.AuditLogUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AuditLogPayload>
}
aggregate: {
args: Prisma.AuditLogAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateAuditLog>
}
groupBy: {
args: Prisma.AuditLogGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AuditLogGroupByOutputType>[]
}
count: {
args: Prisma.AuditLogCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AuditLogCountAggregateOutputType> | number
}
}
}
Schedule: {
payload: Prisma.$SchedulePayload<ExtArgs>
fields: Prisma.ScheduleFieldRefs
operations: {
findUnique: {
args: Prisma.ScheduleFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SchedulePayload> | null
}
findUniqueOrThrow: {
args: Prisma.ScheduleFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SchedulePayload>
}
findFirst: {
args: Prisma.ScheduleFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SchedulePayload> | null
}
findFirstOrThrow: {
args: Prisma.ScheduleFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SchedulePayload>
}
findMany: {
args: Prisma.ScheduleFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SchedulePayload>[]
}
create: {
args: Prisma.ScheduleCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SchedulePayload>
}
createMany: {
args: Prisma.ScheduleCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.ScheduleCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SchedulePayload>[]
}
delete: {
args: Prisma.ScheduleDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SchedulePayload>
}
update: {
args: Prisma.ScheduleUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SchedulePayload>
}
deleteMany: {
args: Prisma.ScheduleDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.ScheduleUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.ScheduleUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SchedulePayload>[]
}
upsert: {
args: Prisma.ScheduleUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$SchedulePayload>
}
aggregate: {
args: Prisma.ScheduleAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateSchedule>
}
groupBy: {
args: Prisma.ScheduleGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.ScheduleGroupByOutputType>[]
}
count: {
args: Prisma.ScheduleCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.ScheduleCountAggregateOutputType> | number
}
}
}
Block: {
payload: Prisma.$BlockPayload<ExtArgs>
fields: Prisma.BlockFieldRefs
operations: {
findUnique: {
args: Prisma.BlockFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BlockPayload> | null
}
findUniqueOrThrow: {
args: Prisma.BlockFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BlockPayload>
}
findFirst: {
args: Prisma.BlockFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BlockPayload> | null
}
findFirstOrThrow: {
args: Prisma.BlockFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BlockPayload>
}
findMany: {
args: Prisma.BlockFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BlockPayload>[]
}
create: {
args: Prisma.BlockCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BlockPayload>
}
createMany: {
args: Prisma.BlockCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.BlockCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BlockPayload>[]
}
delete: {
args: Prisma.BlockDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BlockPayload>
}
update: {
args: Prisma.BlockUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BlockPayload>
}
deleteMany: {
args: Prisma.BlockDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.BlockUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.BlockUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BlockPayload>[]
}
upsert: {
args: Prisma.BlockUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BlockPayload>
}
aggregate: {
args: Prisma.BlockAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateBlock>
}
groupBy: {
args: Prisma.BlockGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.BlockGroupByOutputType>[]
}
count: {
args: Prisma.BlockCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.BlockCountAggregateOutputType> | number
}
}
}
BlockProclamations: {
payload: Prisma.$BlockProclamationsPayload<ExtArgs>
fields: Prisma.BlockProclamationsFieldRefs
operations: {
findUnique: {
args: Prisma.BlockProclamationsFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BlockProclamationsPayload> | null
}
findUniqueOrThrow: {
args: Prisma.BlockProclamationsFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BlockProclamationsPayload>
}
findFirst: {
args: Prisma.BlockProclamationsFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BlockProclamationsPayload> | null
}
findFirstOrThrow: {
args: Prisma.BlockProclamationsFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BlockProclamationsPayload>
}
findMany: {
args: Prisma.BlockProclamationsFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BlockProclamationsPayload>[]
}
create: {
args: Prisma.BlockProclamationsCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BlockProclamationsPayload>
}
createMany: {
args: Prisma.BlockProclamationsCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.BlockProclamationsCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BlockProclamationsPayload>[]
}
delete: {
args: Prisma.BlockProclamationsDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BlockProclamationsPayload>
}
update: {
args: Prisma.BlockProclamationsUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BlockProclamationsPayload>
}
deleteMany: {
args: Prisma.BlockProclamationsDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.BlockProclamationsUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.BlockProclamationsUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BlockProclamationsPayload>[]
}
upsert: {
args: Prisma.BlockProclamationsUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BlockProclamationsPayload>
}
aggregate: {
args: Prisma.BlockProclamationsAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateBlockProclamations>
}
groupBy: {
args: Prisma.BlockProclamationsGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.BlockProclamationsGroupByOutputType>[]
}
count: {
args: Prisma.BlockProclamationsCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.BlockProclamationsCountAggregateOutputType> | number
}
}
}
}
} & {
other: {
@@ -1659,6 +1884,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 +1970,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 +2007,22 @@ export type SolfeggioFrequencyScalarFieldEnum = (typeof SolfeggioFrequencyScalar
export const ProclamationCategoryScalarFieldEnum = {
id: 'id',
name: 'name',
sortOrder: 'sortOrder',
parentId: 'parentId'
sortOrder: 'sortOrder'
} as const
export type ProclamationCategoryScalarFieldEnum = (typeof ProclamationCategoryScalarFieldEnum)[keyof typeof ProclamationCategoryScalarFieldEnum]
export const ProclamationCategorySubscriptionLevelScalarFieldEnum = {
categoryId: 'categoryId',
subscriptionLevelId: 'subscriptionLevelId'
} as const
export type ProclamationCategorySubscriptionLevelScalarFieldEnum = (typeof ProclamationCategorySubscriptionLevelScalarFieldEnum)[keyof typeof ProclamationCategorySubscriptionLevelScalarFieldEnum]
export const LicenseKeyScalarFieldEnum = {
id: 'id',
key: 'key',
active: 'active',
level: 'level',
userId: 'userId'
createdAt: 'createdAt',
lastValidatedAt: 'lastValidatedAt',
userId: 'userId',
email: 'email',
systemName: 'systemName',
systemNameLower: 'systemNameLower'
} as const
export type LicenseKeyScalarFieldEnum = (typeof LicenseKeyScalarFieldEnum)[keyof typeof LicenseKeyScalarFieldEnum]
@@ -1810,15 +2040,6 @@ export const ApiKeyScalarFieldEnum = {
export type ApiKeyScalarFieldEnum = (typeof ApiKeyScalarFieldEnum)[keyof typeof ApiKeyScalarFieldEnum]
export const SubscriptionLevelScalarFieldEnum = {
id: 'id',
name: 'name',
description: 'description'
} as const
export type SubscriptionLevelScalarFieldEnum = (typeof SubscriptionLevelScalarFieldEnum)[keyof typeof SubscriptionLevelScalarFieldEnum]
export const ColorPaletteScalarFieldEnum = {
solfeggioFrequencyId: 'solfeggioFrequencyId',
colors: 'colors'
@@ -1827,6 +2048,62 @@ export const ColorPaletteScalarFieldEnum = {
export type ColorPaletteScalarFieldEnum = (typeof ColorPaletteScalarFieldEnum)[keyof typeof ColorPaletteScalarFieldEnum]
export const AuditLogScalarFieldEnum = {
id: 'id',
createdAt: 'createdAt',
userId: 'userId',
email: 'email',
roleId: 'roleId',
roleName: 'roleName',
page: 'page',
action: 'action',
entity: 'entity',
entityId: 'entityId',
before: 'before',
after: 'after',
ip: 'ip'
} as const
export type AuditLogScalarFieldEnum = (typeof AuditLogScalarFieldEnum)[keyof typeof AuditLogScalarFieldEnum]
export const ScheduleScalarFieldEnum = {
id: 'id',
licenseKeyId: 'licenseKeyId',
userId: 'userId',
type: 'type',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type ScheduleScalarFieldEnum = (typeof ScheduleScalarFieldEnum)[keyof typeof ScheduleScalarFieldEnum]
export const BlockScalarFieldEnum = {
id: 'id',
scheduleId: 'scheduleId',
day: 'day',
startMinutes: 'startMinutes',
endMinutes: 'endMinutes',
dynamicAudio: 'dynamicAudio',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type BlockScalarFieldEnum = (typeof BlockScalarFieldEnum)[keyof typeof BlockScalarFieldEnum]
export const BlockProclamationsScalarFieldEnum = {
blockId: 'blockId',
proclamationId: 'proclamationId',
sortOrder: 'sortOrder',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type BlockProclamationsScalarFieldEnum = (typeof BlockProclamationsScalarFieldEnum)[keyof typeof BlockProclamationsScalarFieldEnum]
export const SortOrder = {
asc: 'asc',
desc: 'desc'
@@ -1842,6 +2119,14 @@ export const JsonNullValueInput = {
export type JsonNullValueInput = (typeof JsonNullValueInput)[keyof typeof JsonNullValueInput]
export const NullableJsonNullValueInput = {
DbNull: DbNull,
JsonNull: JsonNull
} as const
export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput]
export const QueryMode = {
default: 'default',
insensitive: 'insensitive'
@@ -2067,14 +2352,17 @@ export type GlobalOmitConfig = {
userRole?: Prisma.UserRoleOmit
verification?: Prisma.VerificationOmit
proclamation?: Prisma.ProclamationOmit
proclamationGroup?: Prisma.ProclamationGroupOmit
userProclamations?: Prisma.UserProclamationsOmit
solfeggioFrequency?: Prisma.SolfeggioFrequencyOmit
proclamationCategory?: Prisma.ProclamationCategoryOmit
proclamationCategorySubscriptionLevel?: Prisma.ProclamationCategorySubscriptionLevelOmit
licenseKey?: Prisma.LicenseKeyOmit
apiKey?: Prisma.ApiKeyOmit
subscriptionLevel?: Prisma.SubscriptionLevelOmit
colorPalette?: Prisma.ColorPaletteOmit
auditLog?: Prisma.AuditLogOmit
schedule?: Prisma.ScheduleOmit
block?: Prisma.BlockOmit
blockProclamations?: Prisma.BlockProclamationsOmit
}
/* Types for Logging */
@@ -59,14 +59,17 @@ export const ModelName = {
UserRole: 'UserRole',
Verification: 'Verification',
Proclamation: 'Proclamation',
ProclamationGroup: 'ProclamationGroup',
UserProclamations: 'UserProclamations',
SolfeggioFrequency: 'SolfeggioFrequency',
ProclamationCategory: 'ProclamationCategory',
ProclamationCategorySubscriptionLevel: 'ProclamationCategorySubscriptionLevel',
LicenseKey: 'LicenseKey',
ApiKey: 'ApiKey',
SubscriptionLevel: 'SubscriptionLevel',
ColorPalette: 'ColorPalette'
ColorPalette: 'ColorPalette',
AuditLog: 'AuditLog',
Schedule: 'Schedule',
Block: 'Block',
BlockProclamations: 'BlockProclamations'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
@@ -102,6 +105,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 +191,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 +228,22 @@ export type SolfeggioFrequencyScalarFieldEnum = (typeof SolfeggioFrequencyScalar
export const ProclamationCategoryScalarFieldEnum = {
id: 'id',
name: 'name',
sortOrder: 'sortOrder',
parentId: 'parentId'
sortOrder: 'sortOrder'
} as const
export type ProclamationCategoryScalarFieldEnum = (typeof ProclamationCategoryScalarFieldEnum)[keyof typeof ProclamationCategoryScalarFieldEnum]
export const ProclamationCategorySubscriptionLevelScalarFieldEnum = {
categoryId: 'categoryId',
subscriptionLevelId: 'subscriptionLevelId'
} as const
export type ProclamationCategorySubscriptionLevelScalarFieldEnum = (typeof ProclamationCategorySubscriptionLevelScalarFieldEnum)[keyof typeof ProclamationCategorySubscriptionLevelScalarFieldEnum]
export const LicenseKeyScalarFieldEnum = {
id: 'id',
key: 'key',
active: 'active',
level: 'level',
userId: 'userId'
createdAt: 'createdAt',
lastValidatedAt: 'lastValidatedAt',
userId: 'userId',
email: 'email',
systemName: 'systemName',
systemNameLower: 'systemNameLower'
} as const
export type LicenseKeyScalarFieldEnum = (typeof LicenseKeyScalarFieldEnum)[keyof typeof LicenseKeyScalarFieldEnum]
@@ -253,15 +261,6 @@ export const ApiKeyScalarFieldEnum = {
export type ApiKeyScalarFieldEnum = (typeof ApiKeyScalarFieldEnum)[keyof typeof ApiKeyScalarFieldEnum]
export const SubscriptionLevelScalarFieldEnum = {
id: 'id',
name: 'name',
description: 'description'
} as const
export type SubscriptionLevelScalarFieldEnum = (typeof SubscriptionLevelScalarFieldEnum)[keyof typeof SubscriptionLevelScalarFieldEnum]
export const ColorPaletteScalarFieldEnum = {
solfeggioFrequencyId: 'solfeggioFrequencyId',
colors: 'colors'
@@ -270,6 +269,62 @@ export const ColorPaletteScalarFieldEnum = {
export type ColorPaletteScalarFieldEnum = (typeof ColorPaletteScalarFieldEnum)[keyof typeof ColorPaletteScalarFieldEnum]
export const AuditLogScalarFieldEnum = {
id: 'id',
createdAt: 'createdAt',
userId: 'userId',
email: 'email',
roleId: 'roleId',
roleName: 'roleName',
page: 'page',
action: 'action',
entity: 'entity',
entityId: 'entityId',
before: 'before',
after: 'after',
ip: 'ip'
} as const
export type AuditLogScalarFieldEnum = (typeof AuditLogScalarFieldEnum)[keyof typeof AuditLogScalarFieldEnum]
export const ScheduleScalarFieldEnum = {
id: 'id',
licenseKeyId: 'licenseKeyId',
userId: 'userId',
type: 'type',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type ScheduleScalarFieldEnum = (typeof ScheduleScalarFieldEnum)[keyof typeof ScheduleScalarFieldEnum]
export const BlockScalarFieldEnum = {
id: 'id',
scheduleId: 'scheduleId',
day: 'day',
startMinutes: 'startMinutes',
endMinutes: 'endMinutes',
dynamicAudio: 'dynamicAudio',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type BlockScalarFieldEnum = (typeof BlockScalarFieldEnum)[keyof typeof BlockScalarFieldEnum]
export const BlockProclamationsScalarFieldEnum = {
blockId: 'blockId',
proclamationId: 'proclamationId',
sortOrder: 'sortOrder',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type BlockProclamationsScalarFieldEnum = (typeof BlockProclamationsScalarFieldEnum)[keyof typeof BlockProclamationsScalarFieldEnum]
export const SortOrder = {
asc: 'asc',
desc: 'desc'
@@ -285,6 +340,14 @@ export const JsonNullValueInput = {
export type JsonNullValueInput = (typeof JsonNullValueInput)[keyof typeof JsonNullValueInput]
export const NullableJsonNullValueInput = {
DbNull: DbNull,
JsonNull: JsonNull
} as const
export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput]
export const QueryMode = {
default: 'default',
insensitive: 'insensitive'
+5 -2
View File
@@ -16,12 +16,15 @@ export type * from './models/Account.ts'
export type * from './models/UserRole.ts'
export type * from './models/Verification.ts'
export type * from './models/Proclamation.ts'
export type * from './models/ProclamationGroup.ts'
export type * from './models/UserProclamations.ts'
export type * from './models/SolfeggioFrequency.ts'
export type * from './models/ProclamationCategory.ts'
export type * from './models/ProclamationCategorySubscriptionLevel.ts'
export type * from './models/LicenseKey.ts'
export type * from './models/ApiKey.ts'
export type * from './models/SubscriptionLevel.ts'
export type * from './models/ColorPalette.ts'
export type * from './models/AuditLog.ts'
export type * from './models/Schedule.ts'
export type * from './models/Block.ts'
export type * from './models/BlockProclamations.ts'
export type * from './commonInputTypes.ts'
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+345 -61
View File
@@ -28,24 +28,36 @@ export type LicenseKeyMinAggregateOutputType = {
id: string | null
key: string | null
active: boolean | null
level: string | null
createdAt: Date | null
lastValidatedAt: Date | null
userId: string | null
email: string | null
systemName: string | null
systemNameLower: string | null
}
export type LicenseKeyMaxAggregateOutputType = {
id: string | null
key: string | null
active: boolean | null
level: string | null
createdAt: Date | null
lastValidatedAt: Date | null
userId: string | null
email: string | null
systemName: string | null
systemNameLower: string | null
}
export type LicenseKeyCountAggregateOutputType = {
id: number
key: number
active: number
level: number
createdAt: number
lastValidatedAt: number
userId: number
email: number
systemName: number
systemNameLower: number
_all: number
}
@@ -54,24 +66,36 @@ export type LicenseKeyMinAggregateInputType = {
id?: true
key?: true
active?: true
level?: true
createdAt?: true
lastValidatedAt?: true
userId?: true
email?: true
systemName?: true
systemNameLower?: true
}
export type LicenseKeyMaxAggregateInputType = {
id?: true
key?: true
active?: true
level?: true
createdAt?: true
lastValidatedAt?: true
userId?: true
email?: true
systemName?: true
systemNameLower?: true
}
export type LicenseKeyCountAggregateInputType = {
id?: true
key?: true
active?: true
level?: true
createdAt?: true
lastValidatedAt?: true
userId?: true
email?: true
systemName?: true
systemNameLower?: true
_all?: true
}
@@ -151,8 +175,12 @@ export type LicenseKeyGroupByOutputType = {
id: string
key: string
active: boolean
level: string
userId: string
createdAt: Date
lastValidatedAt: Date | null
userId: string | null
email: string | null
systemName: string | null
systemNameLower: string | null
_count: LicenseKeyCountAggregateOutputType | null
_min: LicenseKeyMinAggregateOutputType | null
_max: LicenseKeyMaxAggregateOutputType | null
@@ -180,38 +208,58 @@ export type LicenseKeyWhereInput = {
id?: Prisma.StringFilter<"LicenseKey"> | string
key?: Prisma.StringFilter<"LicenseKey"> | string
active?: Prisma.BoolFilter<"LicenseKey"> | boolean
level?: Prisma.StringFilter<"LicenseKey"> | string
userId?: Prisma.StringFilter<"LicenseKey"> | string
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
createdAt?: Prisma.DateTimeFilter<"LicenseKey"> | Date | string
lastValidatedAt?: Prisma.DateTimeNullableFilter<"LicenseKey"> | Date | string | null
userId?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
email?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
systemName?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
systemNameLower?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
user?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
schedule?: Prisma.XOR<Prisma.ScheduleNullableScalarRelationFilter, Prisma.ScheduleWhereInput> | null
}
export type LicenseKeyOrderByWithRelationInput = {
id?: Prisma.SortOrder
key?: Prisma.SortOrder
active?: Prisma.SortOrder
level?: Prisma.SortOrder
userId?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
lastValidatedAt?: Prisma.SortOrderInput | Prisma.SortOrder
userId?: Prisma.SortOrderInput | Prisma.SortOrder
email?: Prisma.SortOrderInput | Prisma.SortOrder
systemName?: Prisma.SortOrderInput | Prisma.SortOrder
systemNameLower?: Prisma.SortOrderInput | Prisma.SortOrder
user?: Prisma.UserOrderByWithRelationInput
schedule?: Prisma.ScheduleOrderByWithRelationInput
}
export type LicenseKeyWhereUniqueInput = Prisma.AtLeast<{
id?: string
key?: string
userId_systemNameLower?: Prisma.LicenseKeyUserIdSystemNameLowerCompoundUniqueInput
AND?: Prisma.LicenseKeyWhereInput | Prisma.LicenseKeyWhereInput[]
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>
}, "id" | "key">
createdAt?: Prisma.DateTimeFilter<"LicenseKey"> | Date | string
lastValidatedAt?: Prisma.DateTimeNullableFilter<"LicenseKey"> | Date | string | null
userId?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
email?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
systemName?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
systemNameLower?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
user?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
schedule?: Prisma.XOR<Prisma.ScheduleNullableScalarRelationFilter, Prisma.ScheduleWhereInput> | null
}, "id" | "key" | "userId_systemNameLower">
export type LicenseKeyOrderByWithAggregationInput = {
id?: Prisma.SortOrder
key?: Prisma.SortOrder
active?: Prisma.SortOrder
level?: Prisma.SortOrder
userId?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
lastValidatedAt?: Prisma.SortOrderInput | Prisma.SortOrder
userId?: Prisma.SortOrderInput | Prisma.SortOrder
email?: Prisma.SortOrderInput | Prisma.SortOrder
systemName?: Prisma.SortOrderInput | Prisma.SortOrder
systemNameLower?: Prisma.SortOrderInput | Prisma.SortOrder
_count?: Prisma.LicenseKeyCountOrderByAggregateInput
_max?: Prisma.LicenseKeyMaxOrderByAggregateInput
_min?: Prisma.LicenseKeyMinOrderByAggregateInput
@@ -224,63 +272,99 @@ export type LicenseKeyScalarWhereWithAggregatesInput = {
id?: Prisma.StringWithAggregatesFilter<"LicenseKey"> | string
key?: Prisma.StringWithAggregatesFilter<"LicenseKey"> | string
active?: Prisma.BoolWithAggregatesFilter<"LicenseKey"> | boolean
level?: Prisma.StringWithAggregatesFilter<"LicenseKey"> | string
userId?: Prisma.StringWithAggregatesFilter<"LicenseKey"> | string
createdAt?: Prisma.DateTimeWithAggregatesFilter<"LicenseKey"> | Date | string
lastValidatedAt?: Prisma.DateTimeNullableWithAggregatesFilter<"LicenseKey"> | Date | string | null
userId?: Prisma.StringNullableWithAggregatesFilter<"LicenseKey"> | string | null
email?: Prisma.StringNullableWithAggregatesFilter<"LicenseKey"> | string | null
systemName?: Prisma.StringNullableWithAggregatesFilter<"LicenseKey"> | string | null
systemNameLower?: Prisma.StringNullableWithAggregatesFilter<"LicenseKey"> | string | null
}
export type LicenseKeyCreateInput = {
id?: string
key: string
active: boolean
level: string
user: Prisma.UserCreateNestedOneWithoutLicenseKeysInput
createdAt?: Date | string
lastValidatedAt?: Date | string | null
email?: string | null
systemName?: string | null
systemNameLower?: string | null
user?: Prisma.UserCreateNestedOneWithoutLicenseKeysInput
schedule?: Prisma.ScheduleCreateNestedOneWithoutLicenseKeyInput
}
export type LicenseKeyUncheckedCreateInput = {
id?: string
key: string
active: boolean
level: string
userId: string
createdAt?: Date | string
lastValidatedAt?: Date | string | null
userId?: string | null
email?: string | null
systemName?: string | null
systemNameLower?: string | null
schedule?: Prisma.ScheduleUncheckedCreateNestedOneWithoutLicenseKeyInput
}
export type LicenseKeyUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string
user?: Prisma.UserUpdateOneRequiredWithoutLicenseKeysNestedInput
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
lastValidatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
systemName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
systemNameLower?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
user?: Prisma.UserUpdateOneWithoutLicenseKeysNestedInput
schedule?: Prisma.ScheduleUpdateOneWithoutLicenseKeyNestedInput
}
export type LicenseKeyUncheckedUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string
userId?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
lastValidatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
systemName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
systemNameLower?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
schedule?: Prisma.ScheduleUncheckedUpdateOneWithoutLicenseKeyNestedInput
}
export type LicenseKeyCreateManyInput = {
id?: string
key: string
active: boolean
level: string
userId: string
createdAt?: Date | string
lastValidatedAt?: Date | string | null
userId?: string | null
email?: string | null
systemName?: string | null
systemNameLower?: string | null
}
export type LicenseKeyUpdateManyMutationInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
lastValidatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
systemName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
systemNameLower?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type LicenseKeyUncheckedUpdateManyInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string
userId?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
lastValidatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
systemName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
systemNameLower?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type LicenseKeyListRelationFilter = {
@@ -293,28 +377,50 @@ export type LicenseKeyOrderByRelationAggregateInput = {
_count?: Prisma.SortOrder
}
export type LicenseKeyUserIdSystemNameLowerCompoundUniqueInput = {
userId: string
systemNameLower: string
}
export type LicenseKeyCountOrderByAggregateInput = {
id?: Prisma.SortOrder
key?: Prisma.SortOrder
active?: Prisma.SortOrder
level?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
lastValidatedAt?: Prisma.SortOrder
userId?: Prisma.SortOrder
email?: Prisma.SortOrder
systemName?: Prisma.SortOrder
systemNameLower?: Prisma.SortOrder
}
export type LicenseKeyMaxOrderByAggregateInput = {
id?: Prisma.SortOrder
key?: Prisma.SortOrder
active?: Prisma.SortOrder
level?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
lastValidatedAt?: Prisma.SortOrder
userId?: Prisma.SortOrder
email?: Prisma.SortOrder
systemName?: Prisma.SortOrder
systemNameLower?: Prisma.SortOrder
}
export type LicenseKeyMinOrderByAggregateInput = {
id?: Prisma.SortOrder
key?: Prisma.SortOrder
active?: Prisma.SortOrder
level?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
lastValidatedAt?: Prisma.SortOrder
userId?: Prisma.SortOrder
email?: Prisma.SortOrder
systemName?: Prisma.SortOrder
systemNameLower?: Prisma.SortOrder
}
export type LicenseKeyNullableScalarRelationFilter = {
is?: Prisma.LicenseKeyWhereInput | null
isNot?: Prisma.LicenseKeyWhereInput | null
}
export type LicenseKeyCreateNestedManyWithoutUserInput = {
@@ -359,18 +465,44 @@ export type LicenseKeyUncheckedUpdateManyWithoutUserNestedInput = {
deleteMany?: Prisma.LicenseKeyScalarWhereInput | Prisma.LicenseKeyScalarWhereInput[]
}
export type LicenseKeyCreateNestedOneWithoutScheduleInput = {
create?: Prisma.XOR<Prisma.LicenseKeyCreateWithoutScheduleInput, Prisma.LicenseKeyUncheckedCreateWithoutScheduleInput>
connectOrCreate?: Prisma.LicenseKeyCreateOrConnectWithoutScheduleInput
connect?: Prisma.LicenseKeyWhereUniqueInput
}
export type LicenseKeyUpdateOneWithoutScheduleNestedInput = {
create?: Prisma.XOR<Prisma.LicenseKeyCreateWithoutScheduleInput, Prisma.LicenseKeyUncheckedCreateWithoutScheduleInput>
connectOrCreate?: Prisma.LicenseKeyCreateOrConnectWithoutScheduleInput
upsert?: Prisma.LicenseKeyUpsertWithoutScheduleInput
disconnect?: Prisma.LicenseKeyWhereInput | boolean
delete?: Prisma.LicenseKeyWhereInput | boolean
connect?: Prisma.LicenseKeyWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.LicenseKeyUpdateToOneWithWhereWithoutScheduleInput, Prisma.LicenseKeyUpdateWithoutScheduleInput>, Prisma.LicenseKeyUncheckedUpdateWithoutScheduleInput>
}
export type LicenseKeyCreateWithoutUserInput = {
id?: string
key: string
active: boolean
level: string
createdAt?: Date | string
lastValidatedAt?: Date | string | null
email?: string | null
systemName?: string | null
systemNameLower?: string | null
schedule?: Prisma.ScheduleCreateNestedOneWithoutLicenseKeyInput
}
export type LicenseKeyUncheckedCreateWithoutUserInput = {
id?: string
key: string
active: boolean
level: string
createdAt?: Date | string
lastValidatedAt?: Date | string | null
email?: string | null
systemName?: string | null
systemNameLower?: string | null
schedule?: Prisma.ScheduleUncheckedCreateNestedOneWithoutLicenseKeyInput
}
export type LicenseKeyCreateOrConnectWithoutUserInput = {
@@ -406,36 +538,122 @@ export type LicenseKeyScalarWhereInput = {
id?: Prisma.StringFilter<"LicenseKey"> | string
key?: Prisma.StringFilter<"LicenseKey"> | string
active?: Prisma.BoolFilter<"LicenseKey"> | boolean
level?: Prisma.StringFilter<"LicenseKey"> | string
userId?: Prisma.StringFilter<"LicenseKey"> | string
createdAt?: Prisma.DateTimeFilter<"LicenseKey"> | Date | string
lastValidatedAt?: Prisma.DateTimeNullableFilter<"LicenseKey"> | Date | string | null
userId?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
email?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
systemName?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
systemNameLower?: Prisma.StringNullableFilter<"LicenseKey"> | string | null
}
export type LicenseKeyCreateWithoutScheduleInput = {
id?: string
key: string
active: boolean
createdAt?: Date | string
lastValidatedAt?: Date | string | null
email?: string | null
systemName?: string | null
systemNameLower?: string | null
user?: Prisma.UserCreateNestedOneWithoutLicenseKeysInput
}
export type LicenseKeyUncheckedCreateWithoutScheduleInput = {
id?: string
key: string
active: boolean
createdAt?: Date | string
lastValidatedAt?: Date | string | null
userId?: string | null
email?: string | null
systemName?: string | null
systemNameLower?: string | null
}
export type LicenseKeyCreateOrConnectWithoutScheduleInput = {
where: Prisma.LicenseKeyWhereUniqueInput
create: Prisma.XOR<Prisma.LicenseKeyCreateWithoutScheduleInput, Prisma.LicenseKeyUncheckedCreateWithoutScheduleInput>
}
export type LicenseKeyUpsertWithoutScheduleInput = {
update: Prisma.XOR<Prisma.LicenseKeyUpdateWithoutScheduleInput, Prisma.LicenseKeyUncheckedUpdateWithoutScheduleInput>
create: Prisma.XOR<Prisma.LicenseKeyCreateWithoutScheduleInput, Prisma.LicenseKeyUncheckedCreateWithoutScheduleInput>
where?: Prisma.LicenseKeyWhereInput
}
export type LicenseKeyUpdateToOneWithWhereWithoutScheduleInput = {
where?: Prisma.LicenseKeyWhereInput
data: Prisma.XOR<Prisma.LicenseKeyUpdateWithoutScheduleInput, Prisma.LicenseKeyUncheckedUpdateWithoutScheduleInput>
}
export type LicenseKeyUpdateWithoutScheduleInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
lastValidatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
systemName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
systemNameLower?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
user?: Prisma.UserUpdateOneWithoutLicenseKeysNestedInput
}
export type LicenseKeyUncheckedUpdateWithoutScheduleInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
lastValidatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
systemName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
systemNameLower?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type LicenseKeyCreateManyUserInput = {
id?: string
key: string
active: boolean
level: string
createdAt?: Date | string
lastValidatedAt?: Date | string | null
email?: string | null
systemName?: string | null
systemNameLower?: string | null
}
export type LicenseKeyUpdateWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
lastValidatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
systemName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
systemNameLower?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
schedule?: Prisma.ScheduleUpdateOneWithoutLicenseKeyNestedInput
}
export type LicenseKeyUncheckedUpdateWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
lastValidatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
systemName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
systemNameLower?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
schedule?: Prisma.ScheduleUncheckedUpdateOneWithoutLicenseKeyNestedInput
}
export type LicenseKeyUncheckedUpdateManyWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
key?: Prisma.StringFieldUpdateOperationsInput | string
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
level?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
lastValidatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
systemName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
systemNameLower?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
@@ -444,59 +662,82 @@ export type LicenseKeySelect<ExtArgs extends runtime.Types.Extensions.InternalAr
id?: boolean
key?: boolean
active?: boolean
level?: boolean
createdAt?: boolean
lastValidatedAt?: boolean
userId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
email?: boolean
systemName?: boolean
systemNameLower?: boolean
user?: boolean | Prisma.LicenseKey$userArgs<ExtArgs>
schedule?: boolean | Prisma.LicenseKey$scheduleArgs<ExtArgs>
}, ExtArgs["result"]["licenseKey"]>
export type LicenseKeySelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
key?: boolean
active?: boolean
level?: boolean
createdAt?: boolean
lastValidatedAt?: boolean
userId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
email?: boolean
systemName?: boolean
systemNameLower?: boolean
user?: boolean | Prisma.LicenseKey$userArgs<ExtArgs>
}, ExtArgs["result"]["licenseKey"]>
export type LicenseKeySelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
key?: boolean
active?: boolean
level?: boolean
createdAt?: boolean
lastValidatedAt?: boolean
userId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
email?: boolean
systemName?: boolean
systemNameLower?: boolean
user?: boolean | Prisma.LicenseKey$userArgs<ExtArgs>
}, ExtArgs["result"]["licenseKey"]>
export type LicenseKeySelectScalar = {
id?: boolean
key?: boolean
active?: boolean
level?: boolean
createdAt?: boolean
lastValidatedAt?: boolean
userId?: boolean
email?: boolean
systemName?: boolean
systemNameLower?: boolean
}
export type LicenseKeyOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "key" | "active" | "level" | "userId", ExtArgs["result"]["licenseKey"]>
export type LicenseKeyOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "key" | "active" | "createdAt" | "lastValidatedAt" | "userId" | "email" | "systemName" | "systemNameLower", 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>
schedule?: boolean | Prisma.LicenseKey$scheduleArgs<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
schedule: Prisma.$SchedulePayload<ExtArgs> | null
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string
key: string
active: boolean
level: string
userId: string
createdAt: Date
lastValidatedAt: Date | null
userId: string | null
email: string | null
systemName: string | null
systemNameLower: string | null
}, ExtArgs["result"]["licenseKey"]>
composites: {}
}
@@ -891,7 +1132,8 @@ 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>
schedule<T extends Prisma.LicenseKey$scheduleArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.LicenseKey$scheduleArgs<ExtArgs>>): Prisma.Prisma__ScheduleClient<runtime.Types.Result.GetResult<Prisma.$SchedulePayload<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 +1166,12 @@ export interface LicenseKeyFieldRefs {
readonly id: Prisma.FieldRef<"LicenseKey", 'String'>
readonly key: Prisma.FieldRef<"LicenseKey", 'String'>
readonly active: Prisma.FieldRef<"LicenseKey", 'Boolean'>
readonly level: Prisma.FieldRef<"LicenseKey", 'String'>
readonly createdAt: Prisma.FieldRef<"LicenseKey", 'DateTime'>
readonly lastValidatedAt: Prisma.FieldRef<"LicenseKey", 'DateTime'>
readonly userId: Prisma.FieldRef<"LicenseKey", 'String'>
readonly email: Prisma.FieldRef<"LicenseKey", 'String'>
readonly systemName: Prisma.FieldRef<"LicenseKey", 'String'>
readonly systemNameLower: Prisma.FieldRef<"LicenseKey", 'String'>
}
@@ -1326,6 +1572,44 @@ 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.schedule
*/
export type LicenseKey$scheduleArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Schedule
*/
select?: Prisma.ScheduleSelect<ExtArgs> | null
/**
* Omit specific fields from the Schedule
*/
omit?: Prisma.ScheduleOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.ScheduleInclude<ExtArgs> | null
where?: Prisma.ScheduleWhereInput
}
/**
* LicenseKey without action
*/
+293 -163
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,12 @@ 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>
blockProclamations?: Prisma.BlockProclamationsListRelationFilter
}
export type ProclamationOrderByWithRelationInput = {
@@ -244,11 +245,12 @@ 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
blockProclamations?: Prisma.BlockProclamationsOrderByRelationAggregateInput
}
export type ProclamationWhereUniqueInput = Prisma.AtLeast<{
@@ -260,11 +262,12 @@ 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>
blockProclamations?: Prisma.BlockProclamationsListRelationFilter
}, "id" | "name">
export type ProclamationOrderByWithAggregationInput = {
@@ -273,7 +276,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 +293,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 +302,9 @@ export type ProclamationCreateInput = {
content: string
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
group: Prisma.ProclamationGroupCreateNestedOneWithoutProclamationsInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
subscriptionLevel: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput
blockProclamations?: Prisma.BlockProclamationsCreateNestedManyWithoutProclamationInput
}
export type ProclamationUncheckedCreateInput = {
@@ -309,8 +313,9 @@ export type ProclamationUncheckedCreateInput = {
content: string
solfeggioFrequencyId: number
categoryId: number
subscriptionLevelId: number
groupId: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
blockProclamations?: Prisma.BlockProclamationsUncheckedCreateNestedManyWithoutProclamationInput
}
export type ProclamationUpdateInput = {
@@ -319,8 +324,9 @@ export type ProclamationUpdateInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
group?: Prisma.ProclamationGroupUpdateOneRequiredWithoutProclamationsNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput
blockProclamations?: Prisma.BlockProclamationsUpdateManyWithoutProclamationNestedInput
}
export type ProclamationUncheckedUpdateInput = {
@@ -329,8 +335,9 @@ 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
blockProclamations?: Prisma.BlockProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
}
export type ProclamationCreateManyInput = {
@@ -339,7 +346,7 @@ export type ProclamationCreateManyInput = {
content: string
solfeggioFrequencyId: number
categoryId: number
subscriptionLevelId: number
groupId: number
}
export type ProclamationUpdateManyMutationInput = {
@@ -354,7 +361,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 +370,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 +385,7 @@ export type ProclamationMaxOrderByAggregateInput = {
content?: Prisma.SortOrder
solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder
groupId?: Prisma.SortOrder
}
export type ProclamationMinOrderByAggregateInput = {
@@ -387,18 +394,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 +413,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 +558,76 @@ 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 ProclamationCreateNestedOneWithoutBlockProclamationsInput = {
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutBlockProclamationsInput, Prisma.ProclamationUncheckedCreateWithoutBlockProclamationsInput>
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutBlockProclamationsInput
connect?: Prisma.ProclamationWhereUniqueInput
}
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 ProclamationUpdateOneRequiredWithoutBlockProclamationsNestedInput = {
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutBlockProclamationsInput, Prisma.ProclamationUncheckedCreateWithoutBlockProclamationsInput>
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutBlockProclamationsInput
upsert?: Prisma.ProclamationUpsertWithoutBlockProclamationsInput
connect?: Prisma.ProclamationWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.ProclamationUpdateToOneWithWhereWithoutBlockProclamationsInput, Prisma.ProclamationUpdateWithoutBlockProclamationsInput>, Prisma.ProclamationUncheckedUpdateWithoutBlockProclamationsInput>
}
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 ProclamationCreateWithoutGroupInput = {
id?: string
name: string
content: string
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
blockProclamations?: Prisma.BlockProclamationsCreateNestedManyWithoutProclamationInput
}
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 ProclamationUncheckedCreateWithoutGroupInput = {
id?: string
name: string
content: string
solfeggioFrequencyId: number
categoryId: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
blockProclamations?: Prisma.BlockProclamationsUncheckedCreateNestedManyWithoutProclamationInput
}
export type ProclamationCreateOrConnectWithoutGroupInput = {
where: Prisma.ProclamationWhereUniqueInput
create: Prisma.XOR<Prisma.ProclamationCreateWithoutGroupInput, Prisma.ProclamationUncheckedCreateWithoutGroupInput>
}
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 +636,8 @@ export type ProclamationCreateWithoutUserProclamationsInput = {
content: string
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
subscriptionLevel: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput
group: Prisma.ProclamationGroupCreateNestedOneWithoutProclamationsInput
blockProclamations?: Prisma.BlockProclamationsCreateNestedManyWithoutProclamationInput
}
export type ProclamationUncheckedCreateWithoutUserProclamationsInput = {
@@ -566,7 +646,8 @@ export type ProclamationUncheckedCreateWithoutUserProclamationsInput = {
content: string
solfeggioFrequencyId: number
categoryId: number
subscriptionLevelId: number
groupId: number
blockProclamations?: Prisma.BlockProclamationsUncheckedCreateNestedManyWithoutProclamationInput
}
export type ProclamationCreateOrConnectWithoutUserProclamationsInput = {
@@ -591,7 +672,8 @@ export type ProclamationUpdateWithoutUserProclamationsInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput
group?: Prisma.ProclamationGroupUpdateOneRequiredWithoutProclamationsNestedInput
blockProclamations?: Prisma.BlockProclamationsUpdateManyWithoutProclamationNestedInput
}
export type ProclamationUncheckedUpdateWithoutUserProclamationsInput = {
@@ -600,7 +682,8 @@ export type ProclamationUncheckedUpdateWithoutUserProclamationsInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
groupId?: Prisma.IntFieldUpdateOperationsInput | number
blockProclamations?: Prisma.BlockProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
}
export type ProclamationCreateWithoutSolfeggioFrequencyInput = {
@@ -608,8 +691,9 @@ export type ProclamationCreateWithoutSolfeggioFrequencyInput = {
name: string
content: string
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
group: Prisma.ProclamationGroupCreateNestedOneWithoutProclamationsInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
subscriptionLevel: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput
blockProclamations?: Prisma.BlockProclamationsCreateNestedManyWithoutProclamationInput
}
export type ProclamationUncheckedCreateWithoutSolfeggioFrequencyInput = {
@@ -617,8 +701,9 @@ export type ProclamationUncheckedCreateWithoutSolfeggioFrequencyInput = {
name: string
content: string
categoryId: number
subscriptionLevelId: number
groupId: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
blockProclamations?: Prisma.BlockProclamationsUncheckedCreateNestedManyWithoutProclamationInput
}
export type ProclamationCreateOrConnectWithoutSolfeggioFrequencyInput = {
@@ -647,25 +732,14 @@ 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
blockProclamations?: Prisma.BlockProclamationsCreateNestedManyWithoutProclamationInput
}
export type ProclamationUncheckedCreateWithoutCategoryInput = {
@@ -673,8 +747,9 @@ export type ProclamationUncheckedCreateWithoutCategoryInput = {
name: string
content: string
solfeggioFrequencyId: number
subscriptionLevelId: number
groupId: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
blockProclamations?: Prisma.BlockProclamationsUncheckedCreateNestedManyWithoutProclamationInput
}
export type ProclamationCreateOrConnectWithoutCategoryInput = {
@@ -703,48 +778,96 @@ export type ProclamationUpdateManyWithWhereWithoutCategoryInput = {
data: Prisma.XOR<Prisma.ProclamationUpdateManyMutationInput, Prisma.ProclamationUncheckedUpdateManyWithoutCategoryInput>
}
export type ProclamationCreateWithoutSubscriptionLevelInput = {
export type ProclamationCreateWithoutBlockProclamationsInput = {
id?: string
name: string
content: string
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
group: Prisma.ProclamationGroupCreateNestedOneWithoutProclamationsInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
}
export type ProclamationUncheckedCreateWithoutSubscriptionLevelInput = {
export type ProclamationUncheckedCreateWithoutBlockProclamationsInput = {
id?: string
name: string
content: string
solfeggioFrequencyId: number
categoryId: number
groupId: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
}
export type ProclamationCreateOrConnectWithoutSubscriptionLevelInput = {
export type ProclamationCreateOrConnectWithoutBlockProclamationsInput = {
where: Prisma.ProclamationWhereUniqueInput
create: Prisma.XOR<Prisma.ProclamationCreateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput>
create: Prisma.XOR<Prisma.ProclamationCreateWithoutBlockProclamationsInput, Prisma.ProclamationUncheckedCreateWithoutBlockProclamationsInput>
}
export type ProclamationCreateManySubscriptionLevelInputEnvelope = {
data: Prisma.ProclamationCreateManySubscriptionLevelInput | Prisma.ProclamationCreateManySubscriptionLevelInput[]
skipDuplicates?: boolean
export type ProclamationUpsertWithoutBlockProclamationsInput = {
update: Prisma.XOR<Prisma.ProclamationUpdateWithoutBlockProclamationsInput, Prisma.ProclamationUncheckedUpdateWithoutBlockProclamationsInput>
create: Prisma.XOR<Prisma.ProclamationCreateWithoutBlockProclamationsInput, Prisma.ProclamationUncheckedCreateWithoutBlockProclamationsInput>
where?: Prisma.ProclamationWhereInput
}
export type ProclamationUpsertWithWhereUniqueWithoutSubscriptionLevelInput = {
where: Prisma.ProclamationWhereUniqueInput
update: Prisma.XOR<Prisma.ProclamationUpdateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedUpdateWithoutSubscriptionLevelInput>
create: Prisma.XOR<Prisma.ProclamationCreateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedCreateWithoutSubscriptionLevelInput>
export type ProclamationUpdateToOneWithWhereWithoutBlockProclamationsInput = {
where?: Prisma.ProclamationWhereInput
data: Prisma.XOR<Prisma.ProclamationUpdateWithoutBlockProclamationsInput, Prisma.ProclamationUncheckedUpdateWithoutBlockProclamationsInput>
}
export type ProclamationUpdateWithWhereUniqueWithoutSubscriptionLevelInput = {
where: Prisma.ProclamationWhereUniqueInput
data: Prisma.XOR<Prisma.ProclamationUpdateWithoutSubscriptionLevelInput, Prisma.ProclamationUncheckedUpdateWithoutSubscriptionLevelInput>
export type ProclamationUpdateWithoutBlockProclamationsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
group?: Prisma.ProclamationGroupUpdateOneRequiredWithoutProclamationsNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
}
export type ProclamationUpdateManyWithWhereWithoutSubscriptionLevelInput = {
where: Prisma.ProclamationScalarWhereInput
data: Prisma.XOR<Prisma.ProclamationUpdateManyMutationInput, Prisma.ProclamationUncheckedUpdateManyWithoutSubscriptionLevelInput>
export type ProclamationUncheckedUpdateWithoutBlockProclamationsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
groupId?: Prisma.IntFieldUpdateOperationsInput | number
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
}
export type ProclamationCreateManyGroupInput = {
id?: string
name: string
content: string
solfeggioFrequencyId: number
categoryId: number
}
export type ProclamationUpdateWithoutGroupInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
blockProclamations?: Prisma.BlockProclamationsUpdateManyWithoutProclamationNestedInput
}
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
blockProclamations?: Prisma.BlockProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
}
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 +875,7 @@ export type ProclamationCreateManySolfeggioFrequencyInput = {
name: string
content: string
categoryId: number
subscriptionLevelId: number
groupId: number
}
export type ProclamationUpdateWithoutSolfeggioFrequencyInput = {
@@ -760,8 +883,9 @@ export type ProclamationUpdateWithoutSolfeggioFrequencyInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
group?: Prisma.ProclamationGroupUpdateOneRequiredWithoutProclamationsNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput
blockProclamations?: Prisma.BlockProclamationsUpdateManyWithoutProclamationNestedInput
}
export type ProclamationUncheckedUpdateWithoutSolfeggioFrequencyInput = {
@@ -769,8 +893,9 @@ 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
blockProclamations?: Prisma.BlockProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
}
export type ProclamationUncheckedUpdateManyWithoutSolfeggioFrequencyInput = {
@@ -778,7 +903,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 +911,7 @@ export type ProclamationCreateManyCategoryInput = {
name: string
content: string
solfeggioFrequencyId: number
subscriptionLevelId: number
groupId: number
}
export type ProclamationUpdateWithoutCategoryInput = {
@@ -794,8 +919,9 @@ export type ProclamationUpdateWithoutCategoryInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
group?: Prisma.ProclamationGroupUpdateOneRequiredWithoutProclamationsNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput
blockProclamations?: Prisma.BlockProclamationsUpdateManyWithoutProclamationNestedInput
}
export type ProclamationUncheckedUpdateWithoutCategoryInput = {
@@ -803,8 +929,9 @@ 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
blockProclamations?: Prisma.BlockProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
}
export type ProclamationUncheckedUpdateManyWithoutCategoryInput = {
@@ -812,41 +939,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
}
@@ -856,10 +949,12 @@ export type ProclamationUncheckedUpdateManyWithoutSubscriptionLevelInput = {
export type ProclamationCountOutputType = {
userProclamations: number
blockProclamations: number
}
export type ProclamationCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
userProclamations?: boolean | ProclamationCountOutputTypeCountUserProclamationsArgs
blockProclamations?: boolean | ProclamationCountOutputTypeCountBlockProclamationsArgs
}
/**
@@ -879,6 +974,13 @@ export type ProclamationCountOutputTypeCountUserProclamationsArgs<ExtArgs extend
where?: Prisma.UserProclamationsWhereInput
}
/**
* ProclamationCountOutputType without action
*/
export type ProclamationCountOutputTypeCountBlockProclamationsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.BlockProclamationsWhereInput
}
export type ProclamationSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
@@ -886,11 +988,12 @@ 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>
blockProclamations?: boolean | Prisma.Proclamation$blockProclamationsArgs<ExtArgs>
_count?: boolean | Prisma.ProclamationCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["proclamation"]>
@@ -900,10 +1003,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 +1015,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 +1027,27 @@ 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>
blockProclamations?: boolean | Prisma.Proclamation$blockProclamationsArgs<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 +1055,9 @@ 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>
blockProclamations: Prisma.$BlockProclamationsPayload<ExtArgs>[]
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string
@@ -960,7 +1065,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 +1462,9 @@ 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>
blockProclamations<T extends Prisma.Proclamation$blockProclamationsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Proclamation$blockProclamationsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$BlockProclamationsPayload<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.
@@ -1393,7 +1499,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'>
}
@@ -1818,6 +1924,30 @@ export type Proclamation$userProclamationsArgs<ExtArgs extends runtime.Types.Ext
distinct?: Prisma.UserProclamationsScalarFieldEnum | Prisma.UserProclamationsScalarFieldEnum[]
}
/**
* Proclamation.blockProclamations
*/
export type Proclamation$blockProclamationsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the BlockProclamations
*/
select?: Prisma.BlockProclamationsSelect<ExtArgs> | null
/**
* Omit specific fields from the BlockProclamations
*/
omit?: Prisma.BlockProclamationsOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.BlockProclamationsInclude<ExtArgs> | null
where?: Prisma.BlockProclamationsWhereInput
orderBy?: Prisma.BlockProclamationsOrderByWithRelationInput | Prisma.BlockProclamationsOrderByWithRelationInput[]
cursor?: Prisma.BlockProclamationsWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.BlockProclamationsScalarFieldEnum | Prisma.BlockProclamationsScalarFieldEnum[]
}
/**
* Proclamation without action
*/
@@ -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
File diff suppressed because it is too large Load Diff
+240 -53
View File
@@ -243,9 +243,10 @@ 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
schedules?: Prisma.ScheduleListRelationFilter
}
export type UserOrderByWithRelationInput = {
@@ -260,9 +261,10 @@ export type UserOrderByWithRelationInput = {
role?: Prisma.UserRoleOrderByWithRelationInput
sessions?: Prisma.SessionOrderByRelationAggregateInput
accounts?: Prisma.AccountOrderByRelationAggregateInput
profile?: Prisma.ProfileOrderByWithRelationInput
profiles?: Prisma.ProfileOrderByRelationAggregateInput
licenseKeys?: Prisma.LicenseKeyOrderByRelationAggregateInput
userProclamations?: Prisma.UserProclamationsOrderByRelationAggregateInput
schedules?: Prisma.ScheduleOrderByRelationAggregateInput
}
export type UserWhereUniqueInput = Prisma.AtLeast<{
@@ -280,9 +282,10 @@ 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
schedules?: Prisma.ScheduleListRelationFilter
}, "id" | "email">
export type UserOrderByWithAggregationInput = {
@@ -326,9 +329,10 @@ export type UserCreateInput = {
role?: Prisma.UserRoleCreateNestedOneWithoutUsersInput
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileCreateNestedOneWithoutUserInput
profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput
schedules?: Prisma.ScheduleCreateNestedManyWithoutUserInput
}
export type UserUncheckedCreateInput = {
@@ -342,9 +346,10 @@ export type UserUncheckedCreateInput = {
roleId?: number | null
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileUncheckedCreateNestedOneWithoutUserInput
profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput
schedules?: Prisma.ScheduleUncheckedCreateNestedManyWithoutUserInput
}
export type UserUpdateInput = {
@@ -358,9 +363,10 @@ export type UserUpdateInput = {
role?: Prisma.UserRoleUpdateOneWithoutUsersNestedInput
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUpdateOneWithoutUserNestedInput
profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput
schedules?: Prisma.ScheduleUpdateManyWithoutUserNestedInput
}
export type UserUncheckedUpdateInput = {
@@ -374,9 +380,10 @@ 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
schedules?: Prisma.ScheduleUncheckedUpdateManyWithoutUserNestedInput
}
export type UserCreateManyInput = {
@@ -452,6 +459,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 +503,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 +609,33 @@ 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 UserCreateNestedOneWithoutSchedulesInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutSchedulesInput, Prisma.UserUncheckedCreateWithoutSchedulesInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutSchedulesInput
connect?: Prisma.UserWhereUniqueInput
}
export type UserUpdateOneWithoutSchedulesNestedInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutSchedulesInput, Prisma.UserUncheckedCreateWithoutSchedulesInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutSchedulesInput
upsert?: Prisma.UserUpsertWithoutSchedulesInput
disconnect?: Prisma.UserWhereInput | boolean
delete?: Prisma.UserWhereInput | boolean
connect?: Prisma.UserWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutSchedulesInput, Prisma.UserUpdateWithoutSchedulesInput>, Prisma.UserUncheckedUpdateWithoutSchedulesInput>
}
export type UserCreateWithoutProfilesInput = {
id: string
name: string
email: string
@@ -616,9 +648,10 @@ export type UserCreateWithoutProfileInput = {
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput
schedules?: Prisma.ScheduleCreateNestedManyWithoutUserInput
}
export type UserUncheckedCreateWithoutProfileInput = {
export type UserUncheckedCreateWithoutProfilesInput = {
id: string
name: string
email: string
@@ -631,25 +664,26 @@ export type UserUncheckedCreateWithoutProfileInput = {
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput
schedules?: Prisma.ScheduleUncheckedCreateNestedManyWithoutUserInput
}
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
@@ -662,9 +696,10 @@ export type UserUpdateWithoutProfileInput = {
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput
schedules?: Prisma.ScheduleUpdateManyWithoutUserNestedInput
}
export type UserUncheckedUpdateWithoutProfileInput = {
export type UserUncheckedUpdateWithoutProfilesInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
@@ -677,6 +712,7 @@ export type UserUncheckedUpdateWithoutProfileInput = {
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput
schedules?: Prisma.ScheduleUncheckedUpdateManyWithoutUserNestedInput
}
export type UserCreateWithoutSessionsInput = {
@@ -689,9 +725,10 @@ export type UserCreateWithoutSessionsInput = {
updatedAt?: Date | string
role?: Prisma.UserRoleCreateNestedOneWithoutUsersInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileCreateNestedOneWithoutUserInput
profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput
schedules?: Prisma.ScheduleCreateNestedManyWithoutUserInput
}
export type UserUncheckedCreateWithoutSessionsInput = {
@@ -704,9 +741,10 @@ export type UserUncheckedCreateWithoutSessionsInput = {
updatedAt?: Date | string
roleId?: number | null
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileUncheckedCreateNestedOneWithoutUserInput
profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput
schedules?: Prisma.ScheduleUncheckedCreateNestedManyWithoutUserInput
}
export type UserCreateOrConnectWithoutSessionsInput = {
@@ -735,9 +773,10 @@ 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
schedules?: Prisma.ScheduleUpdateManyWithoutUserNestedInput
}
export type UserUncheckedUpdateWithoutSessionsInput = {
@@ -750,9 +789,10 @@ 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
schedules?: Prisma.ScheduleUncheckedUpdateManyWithoutUserNestedInput
}
export type UserCreateWithoutAccountsInput = {
@@ -765,9 +805,10 @@ export type UserCreateWithoutAccountsInput = {
updatedAt?: Date | string
role?: Prisma.UserRoleCreateNestedOneWithoutUsersInput
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileCreateNestedOneWithoutUserInput
profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput
schedules?: Prisma.ScheduleCreateNestedManyWithoutUserInput
}
export type UserUncheckedCreateWithoutAccountsInput = {
@@ -780,9 +821,10 @@ export type UserUncheckedCreateWithoutAccountsInput = {
updatedAt?: Date | string
roleId?: number | null
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileUncheckedCreateNestedOneWithoutUserInput
profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput
schedules?: Prisma.ScheduleUncheckedCreateNestedManyWithoutUserInput
}
export type UserCreateOrConnectWithoutAccountsInput = {
@@ -811,9 +853,10 @@ 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
schedules?: Prisma.ScheduleUpdateManyWithoutUserNestedInput
}
export type UserUncheckedUpdateWithoutAccountsInput = {
@@ -826,9 +869,10 @@ 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
schedules?: Prisma.ScheduleUncheckedUpdateManyWithoutUserNestedInput
}
export type UserCreateWithoutRoleInput = {
@@ -841,9 +885,10 @@ export type UserCreateWithoutRoleInput = {
updatedAt?: Date | string
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileCreateNestedOneWithoutUserInput
profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput
schedules?: Prisma.ScheduleCreateNestedManyWithoutUserInput
}
export type UserUncheckedCreateWithoutRoleInput = {
@@ -856,9 +901,10 @@ export type UserUncheckedCreateWithoutRoleInput = {
updatedAt?: Date | string
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileUncheckedCreateNestedOneWithoutUserInput
profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput
schedules?: Prisma.ScheduleUncheckedCreateNestedManyWithoutUserInput
}
export type UserCreateOrConnectWithoutRoleInput = {
@@ -912,8 +958,9 @@ export type UserCreateWithoutUserProclamationsInput = {
role?: Prisma.UserRoleCreateNestedOneWithoutUsersInput
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileCreateNestedOneWithoutUserInput
profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput
schedules?: Prisma.ScheduleCreateNestedManyWithoutUserInput
}
export type UserUncheckedCreateWithoutUserProclamationsInput = {
@@ -927,8 +974,9 @@ export type UserUncheckedCreateWithoutUserProclamationsInput = {
roleId?: number | null
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileUncheckedCreateNestedOneWithoutUserInput
profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput
schedules?: Prisma.ScheduleUncheckedCreateNestedManyWithoutUserInput
}
export type UserCreateOrConnectWithoutUserProclamationsInput = {
@@ -958,8 +1006,9 @@ export type UserUpdateWithoutUserProclamationsInput = {
role?: Prisma.UserRoleUpdateOneWithoutUsersNestedInput
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUpdateOneWithoutUserNestedInput
profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput
schedules?: Prisma.ScheduleUpdateManyWithoutUserNestedInput
}
export type UserUncheckedUpdateWithoutUserProclamationsInput = {
@@ -973,8 +1022,9 @@ export type UserUncheckedUpdateWithoutUserProclamationsInput = {
roleId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUncheckedUpdateOneWithoutUserNestedInput
profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput
schedules?: Prisma.ScheduleUncheckedUpdateManyWithoutUserNestedInput
}
export type UserCreateWithoutLicenseKeysInput = {
@@ -988,8 +1038,9 @@ export type UserCreateWithoutLicenseKeysInput = {
role?: Prisma.UserRoleCreateNestedOneWithoutUsersInput
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileCreateNestedOneWithoutUserInput
profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput
schedules?: Prisma.ScheduleCreateNestedManyWithoutUserInput
}
export type UserUncheckedCreateWithoutLicenseKeysInput = {
@@ -1003,8 +1054,9 @@ export type UserUncheckedCreateWithoutLicenseKeysInput = {
roleId?: number | null
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
profile?: Prisma.ProfileUncheckedCreateNestedOneWithoutUserInput
profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput
schedules?: Prisma.ScheduleUncheckedCreateNestedManyWithoutUserInput
}
export type UserCreateOrConnectWithoutLicenseKeysInput = {
@@ -1034,8 +1086,9 @@ export type UserUpdateWithoutLicenseKeysInput = {
role?: Prisma.UserRoleUpdateOneWithoutUsersNestedInput
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUpdateOneWithoutUserNestedInput
profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput
schedules?: Prisma.ScheduleUpdateManyWithoutUserNestedInput
}
export type UserUncheckedUpdateWithoutLicenseKeysInput = {
@@ -1049,7 +1102,88 @@ export type UserUncheckedUpdateWithoutLicenseKeysInput = {
roleId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
profile?: Prisma.ProfileUncheckedUpdateOneWithoutUserNestedInput
profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput
schedules?: Prisma.ScheduleUncheckedUpdateManyWithoutUserNestedInput
}
export type UserCreateWithoutSchedulesInput = {
id: string
name: string
email: string
emailVerified?: boolean
image?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: Prisma.UserRoleCreateNestedOneWithoutUsersInput
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput
}
export type UserUncheckedCreateWithoutSchedulesInput = {
id: string
name: string
email: string
emailVerified?: boolean
image?: string | null
createdAt?: Date | string
updatedAt?: Date | string
roleId?: number | null
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput
licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput
}
export type UserCreateOrConnectWithoutSchedulesInput = {
where: Prisma.UserWhereUniqueInput
create: Prisma.XOR<Prisma.UserCreateWithoutSchedulesInput, Prisma.UserUncheckedCreateWithoutSchedulesInput>
}
export type UserUpsertWithoutSchedulesInput = {
update: Prisma.XOR<Prisma.UserUpdateWithoutSchedulesInput, Prisma.UserUncheckedUpdateWithoutSchedulesInput>
create: Prisma.XOR<Prisma.UserCreateWithoutSchedulesInput, Prisma.UserUncheckedCreateWithoutSchedulesInput>
where?: Prisma.UserWhereInput
}
export type UserUpdateToOneWithWhereWithoutSchedulesInput = {
where?: Prisma.UserWhereInput
data: Prisma.XOR<Prisma.UserUpdateWithoutSchedulesInput, Prisma.UserUncheckedUpdateWithoutSchedulesInput>
}
export type UserUpdateWithoutSchedulesInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.UserRoleUpdateOneWithoutUsersNestedInput
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput
}
export type UserUncheckedUpdateWithoutSchedulesInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
roleId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput
licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput
}
@@ -1073,9 +1207,10 @@ 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
schedules?: Prisma.ScheduleUpdateManyWithoutUserNestedInput
}
export type UserUncheckedUpdateWithoutRoleInput = {
@@ -1088,9 +1223,10 @@ 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
schedules?: Prisma.ScheduleUncheckedUpdateManyWithoutUserNestedInput
}
export type UserUncheckedUpdateManyWithoutRoleInput = {
@@ -1111,15 +1247,19 @@ export type UserUncheckedUpdateManyWithoutRoleInput = {
export type UserCountOutputType = {
sessions: number
accounts: number
profiles: number
licenseKeys: number
userProclamations: number
schedules: number
}
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
schedules?: boolean | UserCountOutputTypeCountSchedulesArgs
}
/**
@@ -1146,6 +1286,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
*/
@@ -1160,6 +1307,13 @@ export type UserCountOutputTypeCountUserProclamationsArgs<ExtArgs extends runtim
where?: Prisma.UserProclamationsWhereInput
}
/**
* UserCountOutputType without action
*/
export type UserCountOutputTypeCountSchedulesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.ScheduleWhereInput
}
export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
@@ -1173,9 +1327,10 @@ 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>
schedules?: boolean | Prisma.User$schedulesArgs<ExtArgs>
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["user"]>
@@ -1219,9 +1374,10 @@ 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>
schedules?: boolean | Prisma.User$schedulesArgs<ExtArgs>
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
}
export type UserIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
@@ -1237,9 +1393,10 @@ 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>[]
schedules: Prisma.$SchedulePayload<ExtArgs>[]
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string
@@ -1647,9 +1804,10 @@ 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>
schedules<T extends Prisma.User$schedulesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$schedulesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$SchedulePayload<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.
@@ -2155,9 +2313,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 +2329,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[]
}
/**
@@ -2221,6 +2384,30 @@ export type User$userProclamationsArgs<ExtArgs extends runtime.Types.Extensions.
distinct?: Prisma.UserProclamationsScalarFieldEnum | Prisma.UserProclamationsScalarFieldEnum[]
}
/**
* User.schedules
*/
export type User$schedulesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Schedule
*/
select?: Prisma.ScheduleSelect<ExtArgs> | null
/**
* Omit specific fields from the Schedule
*/
omit?: Prisma.ScheduleOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.ScheduleInclude<ExtArgs> | null
where?: Prisma.ScheduleWhereInput
orderBy?: Prisma.ScheduleOrderByWithRelationInput | Prisma.ScheduleOrderByWithRelationInput[]
cursor?: Prisma.ScheduleWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.ScheduleScalarFieldEnum | Prisma.ScheduleScalarFieldEnum[]
}
/**
* User without action
*/
+381
View File
@@ -0,0 +1,381 @@
import { describe, it, expect } from 'vitest';
import {
blockDuration,
blockSegments,
findOverlap,
findScheduleOverlap,
fiveMinuteOptions,
formatTime,
hmToMinutes,
isOvernight,
parseTimeInput,
snapMinutes,
toHm,
validateBlock,
validateBlockShape,
validateSchedule,
type BlockInput,
type BlockSibling
} from './schedule-utils';
const input = (overrides: Partial<BlockInput> = {}): BlockInput => ({
startMinutes: 6 * 60,
endMinutes: 8 * 60,
proclamationIds: ['p1'],
...overrides
});
const sibling = (startMinutes: number, endMinutes: number, id = 'b1'): BlockSibling => ({
id,
startMinutes,
endMinutes
});
describe('formatTime', () => {
it('formats 12-hour clock labels', () => {
expect(formatTime(0)).toBe('12:00 AM');
expect(formatTime(6 * 60)).toBe('6:00 AM');
expect(formatTime(8 * 60 + 5)).toBe('8:05 AM');
expect(formatTime(12 * 60)).toBe('12:00 PM');
expect(formatTime(13 * 60 + 30)).toBe('1:30 PM');
expect(formatTime(23 * 60 + 59)).toBe('11:59 PM');
});
});
describe('toHm', () => {
it('formats 24-hour HH:MM strings', () => {
expect(toHm(0)).toBe('00:00');
expect(toHm(6 * 60)).toBe('06:00');
expect(toHm(23 * 60 + 59)).toBe('23:59');
});
});
describe('hmToMinutes', () => {
it('parses 24-hour HH:MM strings', () => {
expect(hmToMinutes('00:00')).toBe(0);
expect(hmToMinutes('07:05')).toBe(7 * 60 + 5);
expect(hmToMinutes('23:59')).toBe(23 * 60 + 59);
});
it('rejects malformed or out-of-range values', () => {
expect(hmToMinutes('7:05')).toBeNull();
expect(hmToMinutes('24:00')).toBeNull();
expect(hmToMinutes('12:60')).toBeNull();
expect(hmToMinutes('')).toBeNull();
});
});
describe('parseTimeInput', () => {
it('parses tolerant 12-hour input', () => {
expect(parseTimeInput('7:05 pm')).toEqual({ ok: true, minutes: 19 * 60 + 5 });
expect(parseTimeInput('7:05PM')).toEqual({ ok: true, minutes: 19 * 60 + 5 });
expect(parseTimeInput('7pm')).toEqual({ ok: true, minutes: 19 * 60 });
expect(parseTimeInput('7:7')).toEqual({ ok: true, minutes: 7 * 60 + 7 });
expect(parseTimeInput('12:00 am')).toEqual({ ok: true, minutes: 0 });
expect(parseTimeInput('12:30 PM')).toEqual({ ok: true, minutes: 12 * 60 + 30 });
expect(parseTimeInput('11:59 pm')).toEqual({ ok: true, minutes: 23 * 60 + 59 });
expect(parseTimeInput('0:30')).toEqual({ ok: true, minutes: 30 });
});
it('rejects ambiguous or out-of-range input', () => {
expect(parseTimeInput('13:00 PM').ok).toBe(false);
expect(parseTimeInput('7:70').ok).toBe(false);
expect(parseTimeInput('25:00').ok).toBe(false);
expect(parseTimeInput('').ok).toBe(false);
expect(parseTimeInput('noon').ok).toBe(false);
});
});
describe('fiveMinuteOptions', () => {
it('lists every 5-minute step across the day', () => {
const options = fiveMinuteOptions();
expect(options[0]).toBe(0);
expect(options[1]).toBe(5);
expect(options[options.length - 1]).toBe(23 * 60 + 55);
expect(options).toHaveLength((24 * 60) / 5);
});
});
describe('snapMinutes', () => {
it('rounds to the nearest 15 minutes and clamps to the day', () => {
expect(snapMinutes(0)).toBe(0);
expect(snapMinutes(7)).toBe(0);
expect(snapMinutes(8)).toBe(15);
expect(snapMinutes(370)).toBe(375);
expect(snapMinutes(-10)).toBe(0);
expect(snapMinutes(2000)).toBe(1440);
});
});
describe('isOvernight', () => {
it('is true when the end clock time is before the start', () => {
expect(isOvernight({ startMinutes: 21 * 60, endMinutes: 9 * 60 })).toBe(true);
expect(isOvernight({ startMinutes: 22 * 60, endMinutes: 0 })).toBe(true);
});
it('is false for same-day ranges and a full-day range', () => {
expect(isOvernight({ startMinutes: 6 * 60, endMinutes: 8 * 60 })).toBe(false);
expect(isOvernight({ startMinutes: 0, endMinutes: 1440 })).toBe(false);
});
});
describe('blockDuration', () => {
it('counts a same-day range directly', () => {
expect(blockDuration({ startMinutes: 6 * 60, endMinutes: 8 * 60 })).toBe(120);
});
it('counts an overnight range as wrapping to the next day', () => {
expect(blockDuration({ startMinutes: 21 * 60, endMinutes: 9 * 60 })).toBe(720);
});
it('treats an end of midnight as wrapping from the start', () => {
expect(blockDuration({ startMinutes: 22 * 60, endMinutes: 0 })).toBe(120);
});
});
describe('blockSegments', () => {
it('returns a single segment for a same-day range', () => {
expect(blockSegments({ startMinutes: 6 * 60, endMinutes: 8 * 60 })).toEqual([[6 * 60, 8 * 60]]);
});
it('splits an overnight range at midnight', () => {
expect(blockSegments({ startMinutes: 21 * 60, endMinutes: 9 * 60 })).toEqual([
[21 * 60, 1440],
[0, 9 * 60]
]);
});
it('returns no segments for a zero-length range', () => {
expect(blockSegments({ startMinutes: 360, endMinutes: 360 })).toEqual([]);
});
});
describe('findOverlap', () => {
it('returns null for adjacent ranges', () => {
expect(
findOverlap({ startMinutes: 8 * 60, endMinutes: 9 * 60 }, [sibling(6 * 60, 8 * 60)])
).toBeNull();
});
it('returns the sibling for a partial overlap', () => {
const other = sibling(7 * 60, 9 * 60);
expect(findOverlap({ startMinutes: 8 * 60, endMinutes: 10 * 60 }, [other])).toBe(other);
});
it('detects an overnight block overlapping an early-morning block', () => {
const other = sibling(8 * 60, 10 * 60);
expect(findOverlap({ startMinutes: 21 * 60, endMinutes: 9 * 60 }, [other])).toBe(other);
});
it('detects an overnight block overlapping a late-evening block', () => {
const other = sibling(20 * 60, 22 * 60);
expect(findOverlap({ startMinutes: 21 * 60, endMinutes: 9 * 60 }, [other])).toBe(other);
});
it('allows an overnight block and its exact complement (adjacent at both ends)', () => {
expect(
findOverlap({ startMinutes: 21 * 60, endMinutes: 9 * 60 }, [sibling(9 * 60, 21 * 60)])
).toBeNull();
});
it('allows an overnight block and a midday block it does not touch', () => {
expect(
findOverlap({ startMinutes: 21 * 60, endMinutes: 9 * 60 }, [sibling(10 * 60, 20 * 60)])
).toBeNull();
});
});
describe('validateBlock', () => {
it('accepts a valid block with no siblings', () => {
expect(validateBlock(input(), [], { isCreate: true })).toEqual({ ok: true });
});
it('rejects equal start and end times', () => {
const result = validateBlock(input({ endMinutes: 6 * 60 }), []);
expect(result).toEqual({
ok: false,
error: "Start and end time can't be the same."
});
});
it('rejects a time outside the 24-hour range', () => {
expect(validateBlock(input({ startMinutes: 23 * 60, endMinutes: 25 * 60 }), [])).toEqual({
ok: false,
error: 'Enter a valid start and end time.'
});
});
it('rejects a negative start time', () => {
expect(validateBlock(input({ startMinutes: -1 }), [])).toEqual({
ok: false,
error: 'Enter a valid start and end time.'
});
});
it('accepts an overnight block', () => {
expect(
validateBlock(input({ startMinutes: 21 * 60, endMinutes: 9 * 60 }), [], {
isCreate: true
})
).toEqual({ ok: true });
});
it('rejects an overnight block that overlaps another and names it', () => {
const result = validateBlock(input({ startMinutes: 21 * 60, endMinutes: 9 * 60 }), [
sibling(8 * 60, 10 * 60)
]);
expect(result).toEqual({
ok: false,
error: 'This overlaps with your 8:00 AM–10:00 AM block.'
});
});
it('requires at least one proclamation', () => {
expect(validateBlock(input({ proclamationIds: [] }), [])).toEqual({
ok: false,
error: 'Select at least one proclamation.'
});
});
it('enforces the four-block maximum on create', () => {
const siblings = [
sibling(0, 60, 'a'),
sibling(60, 120, 'b'),
sibling(120, 180, 'c'),
sibling(180, 240, 'd')
];
const result = validateBlock(input({ startMinutes: 300, endMinutes: 360 }), siblings, {
isCreate: true
});
expect(result).toEqual({ ok: false, error: 'You can have at most 4 blocks.' });
});
it('allows adjacent blocks (end exactly equals next start)', () => {
expect(
validateBlock(input({ startMinutes: 8 * 60, endMinutes: 9 * 60 }), [sibling(6 * 60, 8 * 60)])
).toEqual({
ok: true
});
});
it('rejects an exact duplicate range and names the conflicting block', () => {
const result = validateBlock(input(), [sibling(6 * 60, 8 * 60)]);
expect(result).toEqual({
ok: false,
error: 'This overlaps with your 6:00 AM–8:00 AM block.'
});
});
it('rejects a partial overlap', () => {
const result = validateBlock(input({ startMinutes: 7 * 60, endMinutes: 9 * 60 }), [
sibling(8 * 60, 10 * 60)
]);
expect(result.ok).toBe(false);
});
it('rejects a block that contains another block', () => {
const result = validateBlock(input({ startMinutes: 5 * 60, endMinutes: 11 * 60 }), [
sibling(6 * 60, 8 * 60)
]);
expect(result.ok).toBe(false);
});
it('does not apply the maximum when updating (isCreate false)', () => {
const siblings = [sibling(0, 60, 'a'), sibling(60, 120, 'b'), sibling(120, 180, 'c')];
expect(validateBlock(input({ startMinutes: 300, endMinutes: 360 }), siblings).ok).toBe(true);
});
});
// ─── Schedule-wide validation (daily + weekly) ─────────────────────────
const block = (
startMinutes: number,
endMinutes: number,
day: string | null = null,
proclamationIds: string[] = ['p1']
) => ({ day, startMinutes, endMinutes, proclamationIds });
describe('validateBlockShape', () => {
it('rejects start == end', () => {
expect(validateBlockShape(block(360, 360))).toEqual({
ok: false,
error: "Start and end time can't be the same."
});
});
it('rejects an empty block', () => {
expect(validateBlockShape(block(360, 420, null, []))).toEqual({
ok: false,
error: 'Select at least one proclamation.'
});
});
it('accepts a single-minute block', () => {
expect(validateBlockShape(block(360, 361))).toEqual({ ok: true });
});
});
describe('validateSchedule daily', () => {
it('accepts adjacent blocks (touching boundaries)', () => {
expect(validateSchedule('daily', [block(360, 420), block(420, 510)])).toEqual({ ok: true });
});
it('accepts a single-minute block', () => {
expect(validateSchedule('daily', [block(360, 361)])).toEqual({ ok: true });
});
it('rejects overlapping blocks', () => {
expect(validateSchedule('daily', [block(360, 480), block(420, 540)]).ok).toBe(false);
});
it('rejects a midnight-crossing block that spills into an early block', () => {
// 21:00→09:00 covers [0,540) on the next day; 08:00→08:30 overlaps.
expect(validateSchedule('daily', [block(1260, 540, null), block(480, 510, null)]).ok).toBe(
false
);
});
it('accepts a crossing block and its exact complement', () => {
expect(validateSchedule('daily', [block(1260, 540, null), block(540, 1260, null)])).toEqual({
ok: true
});
});
});
describe('validateSchedule weekly', () => {
it('applies rules per day', () => {
expect(validateSchedule('weekly', [block(360, 420, 'mon'), block(360, 420, 'tue')])).toEqual({
ok: true
});
});
it('rejects overlaps within the same day', () => {
expect(validateSchedule('weekly', [block(360, 480, 'wed'), block(420, 540, 'wed')]).ok).toBe(
false
);
});
it('rejects Sunday midnight-crossing spilling into Monday', () => {
expect(validateSchedule('weekly', [block(1380, 60, 'sun'), block(0, 120, 'mon')]).ok).toBe(
false
);
});
it('accepts a Sunday crossing block adjacent to a Monday block', () => {
expect(validateSchedule('weekly', [block(1380, 60, 'sun'), block(60, 120, 'mon')])).toEqual({
ok: true
});
});
it('rejects a Saturday crossing spilling into Sunday', () => {
expect(validateSchedule('weekly', [block(1380, 60, 'sat'), block(30, 90, 'sun')]).ok).toBe(
false
);
});
});
describe('findScheduleOverlap', () => {
it('returns null for gaps', () => {
expect(findScheduleOverlap('daily', [block(360, 420), block(600, 660)])).toBeNull();
});
});
+398
View File
@@ -0,0 +1,398 @@
// Isomorphic, dependency-free schedule helpers. Used by the server service
// layer, the v2 serializers and the client-side Block modal (for live
// pre-validation), so it must not import Prisma or any server-only code.
export const MAX_BLOCKS = 4;
export const MINUTES_PER_DAY = 24 * 60;
export const SNAP_MINUTES = 15;
export const MINUTE_STEP = 5;
export type BlockSibling = {
id?: string;
startMinutes: number;
endMinutes: number;
};
export type BlockInput = {
startMinutes: number;
endMinutes: number;
proclamationIds: string[];
/** When true the kiosk enables dynamic audio for the block's duration. */
dynamicAudio?: boolean;
};
export type ValidationResult = { ok: true } | { ok: false; error: string };
// ─── Schedule-wide validation (daily + weekly, midnight spillover) ─────
export type ScheduleType = 'daily' | 'weekly';
export const WEEK_DAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] as const;
export type WeekDay = (typeof WEEK_DAYS)[number];
/** A Block as seen by the validator. `day` is null for daily schedules. */
export type ValidationBlock = {
id?: string;
day?: string | null;
startMinutes: number;
endMinutes: number;
proclamationIds?: string[];
};
/** Block length in minutes, counting a wrap past midnight as the next day. */
function blockLinearLength(block: { startMinutes: number; endMinutes: number }): number {
return block.endMinutes > block.startMinutes
? block.endMinutes - block.startMinutes
: block.endMinutes - block.startMinutes + MINUTES_PER_DAY;
}
/** Linear half-open interval `[start, end)` relative to `dayIndex`. */
function dayInterval(
block: { startMinutes: number; endMinutes: number },
dayIndex: number
): [number, number] {
const base = dayIndex * MINUTES_PER_DAY;
const start = base + block.startMinutes;
return [start, start + blockLinearLength(block)];
}
function isCrossing(block: { startMinutes: number; endMinutes: number }): boolean {
return block.endMinutes < block.startMinutes;
}
function blocksForDay(blocks: ValidationBlock[], dayIndex: number): ValidationBlock[] {
const day = WEEK_DAYS[dayIndex];
return blocks.filter((b) => (b.day ?? null) === day);
}
/**
* First overlapping pair of Blocks in a schedule, or null. Implements the
* contract's overlap rules: touching boundaries are fine; a midnight-crossing
* Block spills into the next day (weekly Sunday→Monday) and must not overlap the
* next day's early Blocks.
*/
export function findScheduleOverlap(
type: ScheduleType,
blocks: ValidationBlock[]
): { a: ValidationBlock; b: ValidationBlock } | null {
if (type === 'daily') {
// Daily is a single day list that wraps onto itself: a crossing Block
// spills into the next day's copy of the same list. First check the
// same-day intervals, then every crossing Block against the next day.
const intervals = blocks.map((b) => ({ block: b, interval: dayInterval(b, 0) }));
const sameDay = firstPairOverlap(intervals);
if (sameDay) return sameDay;
for (const crossing of blocks.filter(isCrossing)) {
// The crossing Block's post-midnight tail, expressed in this day's
// 0-based frame (it spans [0, endMinutes)).
const spill: IntervalEntry = {
block: crossing,
interval: [0, crossing.endMinutes]
};
for (const { block, interval } of intervals) {
if (block === crossing) continue;
if (segmentsIntersect(spill.interval, interval)) {
return { a: crossing, b: block };
}
}
}
return null;
}
for (let d = 0; d < WEEK_DAYS.length; d++) {
const dayBlocks = blocksForDay(blocks, d);
const intervals = dayBlocks.map((b) => ({ block: b, interval: dayInterval(b, d) }));
// Same-day overlaps (a crossing Block's late segment vs the same day).
const pair = firstPairOverlap(intervals);
if (pair) return pair;
// Spillover from the previous day's crossing Blocks into this day's early
// Blocks. All spills start at midnight, so their union is [0, max(end)).
const prevIndex = (d - 1 + WEEK_DAYS.length) % WEEK_DAYS.length;
const prevCrossing = blocksForDay(blocks, prevIndex).filter(isCrossing);
for (const prev of prevCrossing) {
const spill: [number, number] = [d * MINUTES_PER_DAY, d * MINUTES_PER_DAY + prev.endMinutes];
for (const { block, interval } of intervals) {
if (segmentsIntersect(spill, interval)) {
return { a: prev, b: block };
}
}
}
}
return null;
}
type IntervalEntry = { block: ValidationBlock; interval: [number, number] };
function firstPairOverlap(
entries: IntervalEntry[]
): { a: ValidationBlock; b: ValidationBlock } | null {
for (let i = 0; i < entries.length; i++) {
for (let j = i + 1; j < entries.length; j++) {
if (segmentsIntersect(entries[i].interval, entries[j].interval)) {
return { a: entries[i].block, b: entries[j].block };
}
}
}
return null;
}
/** Validate one Block's shape (time range + at least one Proclamation). */
export function validateBlockShape(block: ValidationBlock): ValidationResult {
const { startMinutes, endMinutes, proclamationIds } = block;
if (
!Number.isInteger(startMinutes) ||
!Number.isInteger(endMinutes) ||
startMinutes < 0 ||
startMinutes > MINUTES_PER_DAY ||
endMinutes < 0 ||
endMinutes > MINUTES_PER_DAY
) {
return { ok: false, error: 'Enter a valid start and end time.' };
}
if (startMinutes === endMinutes) {
return { ok: false, error: "Start and end time can't be the same." };
}
if (block.day != null && !WEEK_DAYS.includes(block.day as WeekDay)) {
return { ok: false, error: 'Pick a valid day of the week.' };
}
if (proclamationIds !== undefined) {
if (!Array.isArray(proclamationIds) || proclamationIds.length === 0) {
return { ok: false, error: 'Select at least one proclamation.' };
}
}
return { ok: true };
}
/**
* Validate a whole schedule. The server is authoritative; the client calls the
* same function for instant feedback.
*/
export function validateSchedule(type: ScheduleType, blocks: ValidationBlock[]): ValidationResult {
for (const block of blocks) {
const shape = validateBlockShape(block);
if (!shape.ok) return shape;
}
const overlap = findScheduleOverlap(type, blocks);
if (overlap) {
return { ok: false, error: overlapError(overlap.a) };
}
return { ok: true };
}
/**
* Validate a candidate Block against the schedule's other Blocks (which must
* exclude the candidate when editing).
*/
export function validateBlockAgainstSchedule(
type: ScheduleType,
candidate: ValidationBlock,
siblings: ValidationBlock[]
): ValidationResult {
const shape = validateBlockShape(candidate);
if (!shape.ok) return shape;
const overlap = findScheduleOverlap(type, [...siblings, candidate]);
if (overlap) {
return { ok: false, error: overlapError(overlap.a) };
}
return { ok: true };
}
function overlapError(a: ValidationBlock): string {
return `This overlaps with your ${formatTime(a.startMinutes)}–${formatTime(a.endMinutes)} block.`;
}
function normalizeMinutes(minutes: number): number {
return (((Math.trunc(minutes) % MINUTES_PER_DAY) + MINUTES_PER_DAY) % MINUTES_PER_DAY) | 0;
}
/** Format minutes-from-midnight as a 12-hour clock label, e.g. `6:00 AM`. */
export function formatTime(minutes: number): string {
const normalized = normalizeMinutes(minutes);
const hours24 = Math.floor(normalized / 60);
const mins = normalized % 60;
const period = hours24 < 12 ? 'AM' : 'PM';
const hours12 = hours24 % 12 === 0 ? 12 : hours24 % 12;
return `${hours12}:${String(mins).padStart(2, '0')} ${period}`;
}
/** Format minutes-from-midnight as a 24-hour `HH:MM` string. */
export function toHm(minutes: number): string {
const normalized = normalizeMinutes(minutes);
const hours = Math.floor(normalized / 60);
const mins = normalized % 60;
return `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`;
}
/**
* Parse a 24-hour `HH:MM` string into minutes from midnight. Returns null when
* the input is not a valid 24-hour time.
*/
export function hmToMinutes(hm: string): number | null {
const match = /^(\d{2}):(\d{2})$/.exec(hm.trim());
if (!match) return null;
const hours = Number(match[1]);
const minutes = Number(match[2]);
if (hours > 23 || minutes > 59) return null;
return hours * 60 + minutes;
}
export type TimeInputResult = { ok: true; minutes: number } | { ok: false; error: string };
/**
* Parse a user-typed 12-hour clock time into minutes from midnight. Tolerant of
* "7:05 pm", "7:05PM", "7pm", "7:7", "12:00 am". A period is optional; when
* present the hour must be 1-12, so "13:00 PM" is rejected. Minutes may be any
* value 00-59, not just the 5-minute steps.
*/
export function parseTimeInput(raw: string): TimeInputResult {
const text = raw.trim().toLowerCase();
if (!text) return { ok: false, error: 'Enter a time.' };
const match = /^(\d{1,2})(?::(\d{1,2}))?\s*(am|pm|a|p)?$/.exec(text);
if (!match) return { ok: false, error: 'Enter a time like 7:05 PM.' };
const hour = Number(match[1]);
const minute = match[2] === undefined ? 0 : Number(match[2]);
const period = match[3];
if (minute > 59) return { ok: false, error: 'Minutes must be 00-59.' };
if (period) {
if (hour < 1 || hour > 12) {
return { ok: false, error: 'Hour must be 1-12 when AM/PM is given.' };
}
const isPm = period === 'pm' || period === 'p';
const hours24 = (hour % 12) + (isPm ? 12 : 0);
return { ok: true, minutes: hours24 * 60 + minute };
}
if (hour > 23) return { ok: false, error: 'Hour must be 0-23.' };
return { ok: true, minutes: hour * 60 + minute };
}
/** The 5-minute clock options (00:00, 00:05, ..., 23:55) in play order. */
export function fiveMinuteOptions(): number[] {
const options: number[] = [];
for (let minutes = 0; minutes < MINUTES_PER_DAY; minutes += 5) {
options.push(minutes);
}
return options;
}
/** Snap a minute value to the nearest `SNAP_MINUTES` interval, clamped to 24h. */
export function snapMinutes(minutes: number): number {
const snapped = Math.round(minutes / SNAP_MINUTES) * SNAP_MINUTES;
return Math.max(0, Math.min(MINUTES_PER_DAY, snapped));
}
/** A Block whose end clock time is at or before its start wraps past midnight. */
export function isOvernight(block: { startMinutes: number; endMinutes: number }): boolean {
return block.endMinutes <= block.startMinutes;
}
/** Block length in minutes, counting a wrap past midnight as the next day. */
export function blockDuration(block: { startMinutes: number; endMinutes: number }): number {
if (block.endMinutes > block.startMinutes) {
return block.endMinutes - block.startMinutes;
}
return block.endMinutes - block.startMinutes + MINUTES_PER_DAY;
}
/**
* Split a Block into the linear half-open `[start, end)` segments it occupies
* within the 24h day. An overnight block produces two segments (start→midnight
* and midnight→end); a zero-length block produces none.
*/
export function blockSegments(block: {
startMinutes: number;
endMinutes: number;
}): [number, number][] {
if (block.startMinutes < block.endMinutes) {
return [[block.startMinutes, block.endMinutes]];
}
if (block.startMinutes > block.endMinutes) {
return [
[block.startMinutes, MINUTES_PER_DAY],
[0, block.endMinutes]
];
}
return [];
}
function segmentsIntersect(a: [number, number], b: [number, number]): boolean {
return a[0] < b[1] && b[0] < a[1];
}
/**
* First sibling whose occupied time intersects the input. Works for overnight
* (midnight-crossing) blocks by comparing their linear segments; ranges that
* only touch at an endpoint (e.g. 9PM–9AM vs 9AM–9PM) do not overlap.
*/
export function findOverlap(
input: { startMinutes: number; endMinutes: number },
siblings: BlockSibling[]
): BlockSibling | null {
const inputSegments = blockSegments(input);
for (const sibling of siblings) {
const siblingSegments = blockSegments(sibling);
for (const segment of inputSegments) {
for (const other of siblingSegments) {
if (segmentsIntersect(segment, other)) return sibling;
}
}
}
return null;
}
/**
* Validate a Block against the schedule's other Blocks. `siblings` must exclude
* the Block being edited. `isCreate` enables the 4-block maximum (an update
* never increases the count).
*/
export function validateBlock(
input: BlockInput,
siblings: BlockSibling[],
opts: { isCreate?: boolean } = {}
): ValidationResult {
const { startMinutes, endMinutes, proclamationIds } = input;
if (
!Number.isInteger(startMinutes) ||
!Number.isInteger(endMinutes) ||
startMinutes < 0 ||
startMinutes > MINUTES_PER_DAY ||
endMinutes < 0 ||
endMinutes > MINUTES_PER_DAY
) {
return { ok: false, error: 'Enter a valid start and end time.' };
}
// Equal clock times would be a zero- or full-length block; both are
// ambiguous, so reject them. Overnight ranges (end before start) are valid.
if (startMinutes === endMinutes) {
return { ok: false, error: "Start and end time can't be the same." };
}
if (!Array.isArray(proclamationIds) || proclamationIds.length === 0) {
return { ok: false, error: 'Select at least one proclamation.' };
}
if (opts.isCreate && siblings.length >= MAX_BLOCKS) {
return { ok: false, error: `You can have at most ${MAX_BLOCKS} blocks.` };
}
const overlap = findOverlap(input, siblings);
if (overlap) {
return {
ok: false,
error: `This overlaps with your ${formatTime(overlap.startMinutes)}–${formatTime(
overlap.endMinutes
)} block.`
};
}
return { ok: true };
}
+23
View File
@@ -0,0 +1,23 @@
import { AsyncLocalStorage } from 'node:async_hooks';
/**
* The actor for the current request, used by the Prisma audit extension to
* attribute database mutations. Populated in `hooks.server.ts`.
*/
export type AuditContext = {
userId: string | null;
email: string | null;
roleId: number | null;
page: string;
ip: string | null;
};
const storage = new AsyncLocalStorage<AuditContext>();
export function runWithAuditContext<T>(context: AuditContext, fn: () => T): T {
return storage.run(context, fn);
}
export function getAuditContext(): AuditContext | undefined {
return storage.getStore();
}
+78
View File
@@ -0,0 +1,78 @@
import { describe, it, expect } from 'vitest';
import { describeAction, entityIdFor, sanitizeAuditValue, shouldAudit } from './audit';
describe('shouldAudit', () => {
it('audits mutations', () => {
expect(shouldAudit('User', 'create')).toBe(true);
expect(shouldAudit('Profile', 'update')).toBe(true);
expect(shouldAudit('Proclamation', 'delete')).toBe(true);
expect(shouldAudit('LicenseKey', 'updateMany')).toBe(true);
});
it('ignores reads', () => {
expect(shouldAudit('User', 'findMany')).toBe(false);
expect(shouldAudit('User', 'findUnique')).toBe(false);
expect(shouldAudit('User', 'count')).toBe(false);
});
it('ignores the audit log and auth internals', () => {
for (const model of ['AuditLog', 'Session', 'Account', 'Verification']) {
expect(shouldAudit(model, 'create')).toBe(false);
}
});
});
describe('sanitizeAuditValue', () => {
it('redacts sensitive keys', () => {
const out = sanitizeAuditValue({
password: 'hunter2',
token: 'abc',
keyHash: 'deadbeef',
email: 'jane@example.com'
}) as Record<string, unknown>;
expect(out.password).toBe('[redacted]');
expect(out.token).toBe('[redacted]');
expect(out.keyHash).toBe('[redacted]');
expect(out.email).toBe('jane@example.com');
});
it('converts dates to ISO strings', () => {
const date = new Date('2026-01-01T00:00:00.000Z');
expect(sanitizeAuditValue({ at: date })).toEqual({ at: '2026-01-01T00:00:00.000Z' });
});
it('handles arrays and nullish values', () => {
expect(sanitizeAuditValue([1, null, 'a'])).toEqual([1, null, 'a']);
expect(sanitizeAuditValue(null)).toBeNull();
expect(sanitizeAuditValue(undefined)).toBeNull();
});
it('truncates very long strings', () => {
const long = 'a'.repeat(21000);
const out = sanitizeAuditValue(long) as string;
expect(out.length).toBe(20001);
expect(out.endsWith('…')).toBe(true);
});
});
describe('entityIdFor', () => {
it('prefers the result id', () => {
expect(entityIdFor({ where: { id: 'w' } }, { id: 'r' }, { id: 'b' })).toBe('r');
});
it('falls back to where, data, then before', () => {
expect(entityIdFor({ where: { id: 5 } }, null, null)).toBe('5');
expect(entityIdFor({ data: { id: 'd' } }, null, null)).toBe('d');
expect(entityIdFor({}, null, { id: 'b' })).toBe('b');
expect(entityIdFor({}, null, null)).toBeNull();
});
});
describe('describeAction', () => {
it('labels bulk operations', () => {
expect(describeAction('updateMany')).toBe('bulk update');
expect(describeAction('deleteMany')).toBe('bulk delete');
expect(describeAction('create')).toBe('create');
});
});
+87
View File
@@ -0,0 +1,87 @@
/**
* Pure helpers for the audit extension. Kept free of database imports so the
* extension in `$lib/db.ts` can use them without a cycle.
*/
/** Prisma operations that change data. Reads are never audited. */
const MUTATION_OPERATIONS = new Set([
'create',
'createMany',
'createManyAndReturn',
'update',
'updateMany',
'updateManyAndReturn',
'upsert',
'delete',
'deleteMany'
]);
/**
* Models we never audit:
* - `AuditLog` itself (would recurse and is not user data)
* - `Session`/`Account`/`Verification` are high-churn auth internals that hold
* tokens, password hashes and other secrets. Business mutations are audited.
*/
const EXCLUDED_MODELS = new Set(['AuditLog', 'Session', 'Account', 'Verification']);
export function shouldAudit(model: string, operation: string): boolean {
return !EXCLUDED_MODELS.has(model) && MUTATION_OPERATIONS.has(operation);
}
const SENSITIVE_KEY = /pass(word)?|secret|token|hash/i;
/**
* Make a value safe to persist as JSON: dates become ISO strings, sensitive
* keys are redacted, very long strings are truncated, and cycles/depth are
* bounded.
*/
export function sanitizeAuditValue(value: unknown, depth = 0): unknown {
if (value === undefined || value === null) return null;
if (depth > 8) return '[truncated]';
if (value instanceof Date) return value.toISOString();
if (Array.isArray(value)) return value.map((item) => sanitizeAuditValue(item, depth + 1));
if (typeof value === 'object') {
const out: Record<string, unknown> = {};
for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {
out[key] = SENSITIVE_KEY.test(key) ? '[redacted]' : sanitizeAuditValue(nested, depth + 1);
}
return out;
}
if (typeof value === 'string' && value.length > 20000) return `${value.slice(0, 20000)}…`;
return value;
}
/** Best-effort row id for the changed record. */
export function entityIdFor(args: unknown, result: unknown, before: unknown): string | null {
const where = (args as { where?: { id?: unknown } } | null)?.where;
const data = (args as { data?: { id?: unknown } } | null)?.data;
const resultId = (result as { id?: unknown } | null)?.id;
const first = Array.isArray(before) ? before[0] : before;
const beforeId = (first as { id?: unknown } | undefined)?.id;
const id = resultId ?? where?.id ?? data?.id ?? beforeId;
return id === undefined || id === null ? null : String(id);
}
/** Human label for an operation, e.g. `updateMany` → `bulk update`. */
export function describeAction(operation: string): string {
switch (operation) {
case 'create':
return 'create';
case 'update':
return 'update';
case 'delete':
return 'delete';
case 'upsert':
return 'upsert';
case 'createMany':
case 'createManyAndReturn':
return 'bulk create';
case 'updateMany':
case 'updateManyAndReturn':
return 'bulk update';
case 'deleteMany':
return 'bulk delete';
default:
return operation;
}
}
+188
View File
@@ -0,0 +1,188 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { PrismaClient } from '$lib/generated/prisma/client';
import pg from 'pg';
import { PrismaPg } from '@prisma/adapter-pg';
import {
getScheduleForLicense,
setScheduleType,
createBlock,
updateBlock,
copySchedule,
listBlocksForLicense,
scheduleBlocksToInput,
type OwnedLicense
} from '$lib/server/schedule';
import { buildSchedulePayload } from '$lib/server/v2-serializers';
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({ adapter });
const K_A1 = 'ALCE-AAAA-AAAA-0001';
const K_A2 = 'ALCE-AAAA-AAAA-0002';
const K_B1 = 'BOBB-BBBB-BBBB-0001';
const results: { id: string; pass: boolean; note: string }[] = [];
let PROCS: string[] = [];
async function licenseByKey(key: string) {
return prisma.licenseKey.findUnique({ where: { key } });
}
async function ownedFor(email: string): Promise<OwnedLicense[]> {
const user = await prisma.user.findUnique({ where: { email } });
if (!user) throw new Error('no user');
return prisma.licenseKey.findMany({
where: { userId: user.id },
select: { id: true, systemName: true, key: true }
});
}
function payloadFor(schedule: Awaited<ReturnType<typeof getScheduleForLicense>>) {
const type = schedule?.type === 'daily' || schedule?.type === 'weekly' ? schedule.type : null;
return buildSchedulePayload(type, scheduleBlocksToInput(schedule?.blocks ?? []));
}
beforeAll(async () => {
const [ka1, ka2, kb1] = await Promise.all([
licenseByKey(K_A1),
licenseByKey(K_A2),
licenseByKey(K_B1)
]);
await prisma.schedule.deleteMany({
where: { licenseKeyId: { in: [ka1!.id, ka2!.id, kb1!.id] } }
});
const procs = await prisma.proclamation.findMany({
where: { name: { startsWith: 'P5 ' } },
select: { id: true },
orderBy: { name: 'asc' }
});
PROCS = procs.map((p) => p.id);
expect(PROCS.length).toBeGreaterThanOrEqual(4);
});
afterAll(async () => {
console.log('\nP5_RESULTS ' + JSON.stringify(results));
await prisma.$disconnect();
await pool.end();
});
const enabled = process.env.P5_INTEGRATION === '1';
describe.skipIf(!enabled)('Phase 5 end-to-end service scenarios', () => {
it('e: create a daily schedule via the Portal path', async () => {
const ka1 = await licenseByKey(K_A1);
await setScheduleType(ka1!.id, 'daily', { confirmed: true });
await createBlock(ka1!.id, {
startMinutes: 7 * 60,
endMinutes: 9 * 60,
proclamationIds: [PROCS[0]],
dynamicAudio: true
});
await createBlock(ka1!.id, {
startMinutes: 18 * 60,
endMinutes: 20 * 60 + 30,
proclamationIds: [PROCS[1]],
dynamicAudio: false
});
const daily = await getScheduleForLicense(ka1!.id);
const p = payloadFor(daily);
results.push({
id: 'e',
pass: p.type === 'daily' && p.daily.length === 2,
note: `type=${p.type} blocks=${p.daily.length}`
});
expect(p.type).toBe('daily');
expect(p.daily).toHaveLength(2);
});
it('f: weekly adjacent + midnight-crossing + Sunday spillover rejection', async () => {
const kb1 = await licenseByKey(K_B1);
await setScheduleType(kb1!.id, 'weekly', { confirmed: true });
await createBlock(kb1!.id, {
day: 'sun',
startMinutes: 23 * 60,
endMinutes: 60,
proclamationIds: [PROCS[2]]
});
await createBlock(kb1!.id, {
day: 'mon',
startMinutes: 6 * 60,
endMinutes: 7 * 60,
proclamationIds: [PROCS[0]]
});
await createBlock(kb1!.id, {
day: 'mon',
startMinutes: 7 * 60,
endMinutes: 8 * 60 + 30,
proclamationIds: [PROCS[1]]
});
const p = payloadFor(await getScheduleForLicense(kb1!.id));
expect(p.type).toBe('weekly');
expect(p.weekly.sun).toHaveLength(1);
expect(p.weekly.sun[0].overnight).toBe(true);
expect(p.weekly.mon).toHaveLength(2);
expect(p.weekly.mon[0].endMinutes).toBe(p.weekly.mon[1].startMinutes);
// Sunday's 23:00->01:00 spills into Monday [00:00,01:00); a Monday 00:00-00:30 block must be rejected.
const rejected = await createBlock(kb1!.id, {
day: 'mon',
startMinutes: 0,
endMinutes: 30,
proclamationIds: [PROCS[3]]
});
const monCount = (await getScheduleForLicense(kb1!.id))!.blocks.filter(
(b) => b.day === 'mon'
).length;
results.push({
id: 'f',
pass: !rejected.ok && monCount === 2,
note: `sun overnight=${p.weekly.sun[0].overnight} mon adjacent=${p.weekly.mon[0].endMinutes === p.weekly.mon[1].startMinutes} spilloverRejected=${!rejected.ok}`
});
expect(rejected.ok).toBe(false);
expect(monCount).toBe(2);
});
it('i: schedules are per-License-Key (A does not affect B)', async () => {
const [ka1, kb1] = await Promise.all([licenseByKey(K_A1), licenseByKey(K_B1)]);
const a = payloadFor(await getScheduleForLicense(ka1!.id));
const b = payloadFor(await getScheduleForLicense(kb1!.id));
results.push({
id: 'i',
pass: a.type === 'daily' && b.type === 'weekly',
note: `A=${a.type} B=${b.type}`
});
expect(a.type).toBe('daily');
expect(b.type).toBe('weekly');
});
it('j: copy A -> second system and verify independence', async () => {
const [ka1, ka2] = await Promise.all([licenseByKey(K_A1), licenseByKey(K_A2)]);
const owned = await ownedFor('p5-alice@example.com');
const copy = await copySchedule(owned, ka1!.id, [ka2!.id]);
expect(copy.ok).toBe(true);
const ka2Before = payloadFor(await getScheduleForLicense(ka2!.id));
expect(ka2Before.type).toBe('daily');
expect(ka2Before.daily).toHaveLength(2);
const src = payloadFor(await getScheduleForLicense(ka1!.id));
expect(src.version).not.toBe(ka2Before.version);
// Edit source; copy must not change.
const blocks = await listBlocksForLicense(ka1!.id);
await updateBlock(ka1!.id, blocks[0].id, {
startMinutes: 6 * 60,
endMinutes: 7 * 60,
proclamationIds: blocks[0].proclamations.map((p) => p.proclamation.id),
dynamicAudio: true
});
const ka2After = payloadFor(await getScheduleForLicense(ka2!.id));
results.push({
id: 'j',
pass: ka2After.version === ka2Before.version && src.version !== ka2Before.version,
note: `copied daily blocks=${ka2Before.daily.length} independent=${ka2After.version === ka2Before.version}`
});
expect(ka2After.version).toBe(ka2Before.version);
});
});
+26
View File
@@ -0,0 +1,26 @@
import { env } from '$env/dynamic/private';
import { sendMail } from './resend';
/**
* Send the "finish your registration" email to a user created by an admin.
*
* The link points at Better Auth's reset-password callback, which redirects to
* `/reset-password?invite=1&token=...`. There the user sets a password, then
* logs in and is sent to `/profile/setup` to complete their profile.
*
* Uses a Resend-hosted template (`registration-email` by default, or
* `RESEND_REGISTRATION_TEMPLATE_ID`) with variables:
* - `target_email_address`
* - `registration_link`
*/
export async function sendRegistrationEmail(options: { email: string; url: string }) {
await sendMail({
to: options.email,
subject: 'Welcome to Radiant Wave — set your password',
templateId: env.RESEND_REGISTRATION_TEMPLATE_ID || 'registration-email',
data: {
target_email_address: options.email,
registration_link: options.url
}
});
}
+18
View File
@@ -0,0 +1,18 @@
import { auth } from '$lib/auth';
/**
* Email an invited user a link to set their password and finish their profile.
*
* Reuses Better Auth's password-reset token flow; the `invite=1` marker makes
* the reset-password hook send the `registration-email` template instead of the
* usual "you requested a reset" email. The link lands on /reset-password, and
* after setting a password the user logs in and is sent to /profile/setup.
*/
export async function sendRegistrationInvite(email: string): Promise<void> {
await auth.api.requestPasswordReset({
body: {
email,
redirectTo: '/reset-password?invite=1'
}
});
}
+3 -3
View File
@@ -26,7 +26,7 @@ function getResend(): Resend {
* @example
* await sendMail({
* to: 'jane@example.com',
* templateId: 'tmpl_abc123', // optional — defaults to RESEND_TEMPLATE_ID
* templateId: 'tmpl_abc123', // required — Resend template id or alias
* subject: 'Your license key is ready', // optional — defaults to 'Template Email'
* data: { // optional — template variables
* firstName: 'Jane',
@@ -40,7 +40,7 @@ function getResend(): Resend {
export async function sendMail(options: {
to: string;
subject?: string;
templateId?: string;
templateId: string;
data?: Record<string, string | number>;
}) {
const { to, subject, templateId, data } = options;
@@ -50,7 +50,7 @@ export async function sendMail(options: {
to: [to],
subject: subject ?? 'Template Email',
template: {
id: templateId ?? env.RESEND_TEMPLATE_ID,
id: templateId,
variables: data ?? {}
}
});
+28
View File
@@ -0,0 +1,28 @@
/**
* Role hierarchy. Lower ids are more privileged:
* SUPER = 1, ADMIN = 2, STAFF = 3, USER = 4
*
* `roleId` is exposed on the Better Auth session user (see `auth.ts`
* `user.additionalFields`), so these helpers never need a database call.
*/
export const SUPER = 1;
export const ADMIN = 2;
export const STAFF = 3;
export const USER = 4;
/** The current user's role id, defaulting to the least privileged role. */
export function roleId(locals: App.Locals): number {
return locals.user?.roleId ?? USER;
}
export function isSuper(locals: App.Locals): boolean {
return roleId(locals) <= SUPER;
}
export function isAdmin(locals: App.Locals): boolean {
return roleId(locals) <= ADMIN;
}
export function isStaff(locals: App.Locals): boolean {
return roleId(locals) <= STAFF;
}

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