Build API v2
This commit is contained in:
Vendored
+2
@@ -1,10 +1,12 @@
|
||||
import type { Session, User } from 'better-auth';
|
||||
import type { ApiKeyRecord } from '$lib/server/api-key';
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
interface Locals {
|
||||
session: Session | null;
|
||||
user: User | null;
|
||||
apiKey?: ApiKeyRecord;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+103
-8
@@ -1,4 +1,5 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import type { RequestEvent } from '@sveltejs/kit';
|
||||
|
||||
const { getSession } = vi.hoisted(() => ({
|
||||
getSession: vi.fn()
|
||||
@@ -8,10 +9,19 @@ const { svelteKitHandler } = vi.hoisted(() => ({
|
||||
svelteKitHandler: vi.fn()
|
||||
}));
|
||||
|
||||
const { validateApiKeyMock, envMock } = vi.hoisted(() => ({
|
||||
validateApiKeyMock: vi.fn(),
|
||||
envMock: { API_KEYS_ACTIVE: undefined as string | undefined }
|
||||
}));
|
||||
|
||||
vi.mock('$app/environment', () => ({
|
||||
building: false
|
||||
}));
|
||||
|
||||
vi.mock('$env/dynamic/private', () => ({
|
||||
env: envMock
|
||||
}));
|
||||
|
||||
vi.mock('better-auth/svelte-kit', () => ({
|
||||
svelteKitHandler
|
||||
}));
|
||||
@@ -25,24 +35,33 @@ vi.mock('$lib/auth', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('$lib/server/api-key', () => ({
|
||||
validateApiKey: vi.fn()
|
||||
validateApiKey: validateApiKeyMock
|
||||
}));
|
||||
|
||||
import { handle } from './hooks.server';
|
||||
|
||||
function createMockEvent(path = '/dashboard') {
|
||||
type MockRequestEvent = RequestEvent & { resolve: ReturnType<typeof vi.fn> };
|
||||
|
||||
function createMockEvent(path = '/dashboard'): MockRequestEvent {
|
||||
return {
|
||||
request: new Request(`http://localhost${path}`),
|
||||
url: new URL(`http://localhost${path}`),
|
||||
locals: {} as Record<string, unknown>,
|
||||
resolve: vi.fn()
|
||||
} as any;
|
||||
} as unknown as MockRequestEvent;
|
||||
}
|
||||
|
||||
function runHandle(event: MockRequestEvent) {
|
||||
type HandleArgs = Parameters<typeof handle>[0];
|
||||
return handle({ event, resolve: event.resolve as unknown as HandleArgs['resolve'] });
|
||||
}
|
||||
|
||||
describe('hooks.server handle', () => {
|
||||
beforeEach(() => {
|
||||
getSession.mockReset();
|
||||
svelteKitHandler.mockReset();
|
||||
validateApiKeyMock.mockReset();
|
||||
envMock.API_KEYS_ACTIVE = undefined;
|
||||
});
|
||||
|
||||
it('sets session and user on locals when authenticated', async () => {
|
||||
@@ -53,7 +72,7 @@ describe('hooks.server handle', () => {
|
||||
getSession.mockResolvedValue(sessionData);
|
||||
|
||||
const event = createMockEvent();
|
||||
await handle({ event, resolve: event.resolve });
|
||||
await runHandle(event);
|
||||
|
||||
expect(event.locals.session).toEqual(sessionData.session);
|
||||
expect(event.locals.user).toEqual(sessionData.user);
|
||||
@@ -70,7 +89,7 @@ describe('hooks.server handle', () => {
|
||||
getSession.mockResolvedValue(null);
|
||||
|
||||
const event = createMockEvent();
|
||||
await handle({ event, resolve: event.resolve });
|
||||
await runHandle(event);
|
||||
|
||||
expect(event.locals.session).toBeUndefined();
|
||||
expect(event.locals.user).toBeUndefined();
|
||||
@@ -80,7 +99,7 @@ describe('hooks.server handle', () => {
|
||||
getSession.mockResolvedValue(undefined);
|
||||
|
||||
const event = createMockEvent();
|
||||
await handle({ event, resolve: event.resolve });
|
||||
await runHandle(event);
|
||||
|
||||
expect(event.locals.session).toBeUndefined();
|
||||
expect(event.locals.user).toBeUndefined();
|
||||
@@ -90,7 +109,7 @@ describe('hooks.server handle', () => {
|
||||
getSession.mockResolvedValue(null);
|
||||
|
||||
const event = createMockEvent();
|
||||
await handle({ event, resolve: event.resolve });
|
||||
await runHandle(event);
|
||||
|
||||
expect(svelteKitHandler).toHaveBeenCalledWith(expect.objectContaining({ building: false }));
|
||||
});
|
||||
@@ -99,7 +118,7 @@ describe('hooks.server handle', () => {
|
||||
getSession.mockResolvedValue(null);
|
||||
|
||||
const event = createMockEvent();
|
||||
await handle({ event, resolve: event.resolve });
|
||||
await runHandle(event);
|
||||
|
||||
expect(svelteKitHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -109,4 +128,80 @@ describe('hooks.server handle', () => {
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('v2 auth gate', () => {
|
||||
it('returns 401 for v2 routes without an Authorization header', async () => {
|
||||
const event = createMockEvent('/api/v2/proclamations');
|
||||
const res = await runHandle(event);
|
||||
|
||||
expect(res.status).toBe(401);
|
||||
expect(await res.json()).toEqual({ error: 'API key required' });
|
||||
expect(validateApiKeyMock).not.toHaveBeenCalled();
|
||||
expect(event.resolve).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns 401 for v2 routes with an invalid bearer key', async () => {
|
||||
validateApiKeyMock.mockResolvedValue(null);
|
||||
|
||||
const event = createMockEvent('/api/v2/proclamations');
|
||||
event.request = new Request('http://localhost/api/v2/proclamations', {
|
||||
headers: { authorization: 'Bearer sk_invalid' }
|
||||
});
|
||||
const res = await runHandle(event);
|
||||
|
||||
expect(res.status).toBe(401);
|
||||
expect(await res.json()).toEqual({ error: 'Invalid or expired API key' });
|
||||
expect(validateApiKeyMock).toHaveBeenCalledWith('sk_invalid');
|
||||
expect(event.resolve).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('resolves v2 routes with a valid bearer key and sets locals.apiKey', async () => {
|
||||
validateApiKeyMock.mockResolvedValue({ id: 'key-1', keyPrefix: 'sk_abc123' });
|
||||
|
||||
const event = createMockEvent('/api/v2/proclamations');
|
||||
event.request = new Request('http://localhost/api/v2/proclamations', {
|
||||
headers: { authorization: 'Bearer sk_abc123def' }
|
||||
});
|
||||
await runHandle(event);
|
||||
|
||||
expect(event.resolve).toHaveBeenCalled();
|
||||
expect(event.locals.apiKey).toEqual({ id: 'key-1', keyPrefix: 'sk_abc123' });
|
||||
expect(validateApiKeyMock).toHaveBeenCalledWith('sk_abc123def');
|
||||
});
|
||||
|
||||
it('always validates the v2 API key even when API_KEYS_ACTIVE would bypass v1', async () => {
|
||||
envMock.API_KEYS_ACTIVE = 'false';
|
||||
validateApiKeyMock.mockResolvedValue(null);
|
||||
|
||||
const event = createMockEvent('/api/v2/frequencies');
|
||||
event.request = new Request('http://localhost/api/v2/frequencies', {
|
||||
headers: { authorization: 'Bearer sk_abc' }
|
||||
});
|
||||
const res = await runHandle(event);
|
||||
|
||||
expect(res.status).toBe(401);
|
||||
expect(await res.json()).toEqual({ error: 'Invalid or expired API key' });
|
||||
expect(validateApiKeyMock).toHaveBeenCalledWith('sk_abc');
|
||||
});
|
||||
|
||||
it('does not require an API key for signed v2 onboarding routes', async () => {
|
||||
getSession.mockResolvedValue(null);
|
||||
|
||||
const event = createMockEvent('/api/v2/automations/create/account');
|
||||
await runHandle(event);
|
||||
|
||||
expect(validateApiKeyMock).not.toHaveBeenCalled();
|
||||
expect(svelteKitHandler).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not apply the v2 gate to non-API routes', async () => {
|
||||
getSession.mockResolvedValue(null);
|
||||
|
||||
const event = createMockEvent('/dashboard');
|
||||
await runHandle(event);
|
||||
|
||||
expect(validateApiKeyMock).not.toHaveBeenCalled();
|
||||
expect(svelteKitHandler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,34 @@ import { validateApiKey } from '$lib/server/api-key';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
export async function handle({ event, resolve }) {
|
||||
// v2 auth is ALWAYS enforced — no API_KEYS_ACTIVE bypass. The only
|
||||
// exceptions are the signed onboarding routes (HMAC-protected).
|
||||
const isV2 = event.url.pathname.startsWith('/api/v2/');
|
||||
const isSignedOnboarding = event.url.pathname.startsWith('/api/v2/automations/');
|
||||
|
||||
if (isV2 && !isSignedOnboarding) {
|
||||
const authHeader = event.request.headers.get('authorization');
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
return new Response(JSON.stringify({ error: 'API key required' }), {
|
||||
status: 401,
|
||||
headers: { 'content-type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const key = authHeader.slice(7);
|
||||
const record = await validateApiKey(key);
|
||||
if (!record) {
|
||||
return new Response(JSON.stringify({ error: 'Invalid or expired API key' }), {
|
||||
status: 401,
|
||||
headers: { 'content-type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
event.locals.apiKey = record;
|
||||
|
||||
return resolve(event);
|
||||
}
|
||||
|
||||
if (
|
||||
event.url.pathname.startsWith('/api/v1/') &&
|
||||
!event.url.pathname.startsWith('/api/v1/automations/')
|
||||
|
||||
@@ -1,94 +1,99 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file should be your main import to use Prisma-related types and utilities in a browser.
|
||||
* This file should be your main import to use Prisma-related types and utilities in a browser.
|
||||
* Use it to get access to models, enums, and input types.
|
||||
*
|
||||
*
|
||||
* This file does not contain a `PrismaClient` class, nor several other helpers that are intended as server-side only.
|
||||
* See `client.ts` for the standard, server-side entry point.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
import * as Prisma from './internal/prismaNamespaceBrowser.ts';
|
||||
export { Prisma };
|
||||
export * as $Enums from './enums.ts';
|
||||
import * as Prisma from './internal/prismaNamespaceBrowser.ts'
|
||||
export { Prisma }
|
||||
export * as $Enums from './enums.ts'
|
||||
export * from './enums.ts';
|
||||
/**
|
||||
* Model User
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type User = Prisma.UserModel;
|
||||
export type User = Prisma.UserModel
|
||||
/**
|
||||
* Model Profile
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type Profile = Prisma.ProfileModel;
|
||||
export type Profile = Prisma.ProfileModel
|
||||
/**
|
||||
* Model Address
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type Address = Prisma.AddressModel;
|
||||
export type Address = Prisma.AddressModel
|
||||
/**
|
||||
* Model Session
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type Session = Prisma.SessionModel;
|
||||
export type Session = Prisma.SessionModel
|
||||
/**
|
||||
* Model Account
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type Account = Prisma.AccountModel;
|
||||
export type Account = Prisma.AccountModel
|
||||
/**
|
||||
* Model UserRole
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type UserRole = Prisma.UserRoleModel;
|
||||
export type UserRole = Prisma.UserRoleModel
|
||||
/**
|
||||
* Model Verification
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type Verification = Prisma.VerificationModel;
|
||||
export type Verification = Prisma.VerificationModel
|
||||
/**
|
||||
* Model Proclamation
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type Proclamation = Prisma.ProclamationModel;
|
||||
export type Proclamation = Prisma.ProclamationModel
|
||||
/**
|
||||
* Model UserProclamations
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type UserProclamations = Prisma.UserProclamationsModel;
|
||||
export type UserProclamations = Prisma.UserProclamationsModel
|
||||
/**
|
||||
* Model SolfeggioFrequency
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type SolfeggioFrequency = Prisma.SolfeggioFrequencyModel;
|
||||
export type SolfeggioFrequency = Prisma.SolfeggioFrequencyModel
|
||||
/**
|
||||
* Model ProclamationCategory
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type ProclamationCategory = Prisma.ProclamationCategoryModel;
|
||||
export type ProclamationCategory = Prisma.ProclamationCategoryModel
|
||||
/**
|
||||
* Model ProclamationCategorySubscriptionLevel
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type ProclamationCategorySubscriptionLevel =
|
||||
Prisma.ProclamationCategorySubscriptionLevelModel;
|
||||
export type ProclamationCategorySubscriptionLevel = Prisma.ProclamationCategorySubscriptionLevelModel
|
||||
/**
|
||||
* Model LicenseKey
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type LicenseKey = Prisma.LicenseKeyModel;
|
||||
export type LicenseKey = Prisma.LicenseKeyModel
|
||||
/**
|
||||
* Model ApiKey
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type ApiKey = Prisma.ApiKeyModel;
|
||||
export type ApiKey = Prisma.ApiKeyModel
|
||||
/**
|
||||
* Model SubscriptionLevel
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type SubscriptionLevel = Prisma.SubscriptionLevelModel;
|
||||
export type SubscriptionLevel = Prisma.SubscriptionLevelModel
|
||||
/**
|
||||
* Model ColorPalette
|
||||
*
|
||||
*/
|
||||
export type ColorPalette = Prisma.ColorPaletteModel
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types.
|
||||
* If you're looking for something you can import in the client-side of your application, please refer to the `browser.ts` file instead.
|
||||
@@ -9,21 +10,21 @@
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
import * as process from 'node:process';
|
||||
import * as path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url));
|
||||
import * as process from 'node:process'
|
||||
import * as path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
import * as runtime from '@prisma/client/runtime/client';
|
||||
import * as $Enums from './enums.ts';
|
||||
import * as $Class from './internal/class.ts';
|
||||
import * as Prisma from './internal/prismaNamespace.ts';
|
||||
import * as runtime from "@prisma/client/runtime/client"
|
||||
import * as $Enums from "./enums.ts"
|
||||
import * as $Class from "./internal/class.ts"
|
||||
import * as Prisma from "./internal/prismaNamespace.ts"
|
||||
|
||||
export * as $Enums from './enums.ts';
|
||||
export * from './enums.ts';
|
||||
export * as $Enums from './enums.ts'
|
||||
export * from "./enums.ts"
|
||||
/**
|
||||
* ## Prisma Client
|
||||
*
|
||||
*
|
||||
* Type-safe database client for TypeScript
|
||||
* @example
|
||||
* ```
|
||||
@@ -33,90 +34,90 @@ export * from './enums.ts';
|
||||
* // Fetch zero or more Users
|
||||
* const users = await prisma.user.findMany()
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* Read more in our [docs](https://pris.ly/d/client).
|
||||
*/
|
||||
export const PrismaClient = $Class.getPrismaClientClass();
|
||||
export type PrismaClient<
|
||||
LogOpts extends Prisma.LogLevel = never,
|
||||
OmitOpts extends Prisma.PrismaClientOptions['omit'] = Prisma.PrismaClientOptions['omit'],
|
||||
ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
|
||||
> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>;
|
||||
export { Prisma };
|
||||
export const PrismaClient = $Class.getPrismaClientClass()
|
||||
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
||||
export { Prisma }
|
||||
|
||||
/**
|
||||
* Model User
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type User = Prisma.UserModel;
|
||||
export type User = Prisma.UserModel
|
||||
/**
|
||||
* Model Profile
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type Profile = Prisma.ProfileModel;
|
||||
export type Profile = Prisma.ProfileModel
|
||||
/**
|
||||
* Model Address
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type Address = Prisma.AddressModel;
|
||||
export type Address = Prisma.AddressModel
|
||||
/**
|
||||
* Model Session
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type Session = Prisma.SessionModel;
|
||||
export type Session = Prisma.SessionModel
|
||||
/**
|
||||
* Model Account
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type Account = Prisma.AccountModel;
|
||||
export type Account = Prisma.AccountModel
|
||||
/**
|
||||
* Model UserRole
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type UserRole = Prisma.UserRoleModel;
|
||||
export type UserRole = Prisma.UserRoleModel
|
||||
/**
|
||||
* Model Verification
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type Verification = Prisma.VerificationModel;
|
||||
export type Verification = Prisma.VerificationModel
|
||||
/**
|
||||
* Model Proclamation
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type Proclamation = Prisma.ProclamationModel;
|
||||
export type Proclamation = Prisma.ProclamationModel
|
||||
/**
|
||||
* Model UserProclamations
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type UserProclamations = Prisma.UserProclamationsModel;
|
||||
export type UserProclamations = Prisma.UserProclamationsModel
|
||||
/**
|
||||
* Model SolfeggioFrequency
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type SolfeggioFrequency = Prisma.SolfeggioFrequencyModel;
|
||||
export type SolfeggioFrequency = Prisma.SolfeggioFrequencyModel
|
||||
/**
|
||||
* Model ProclamationCategory
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type ProclamationCategory = Prisma.ProclamationCategoryModel;
|
||||
export type ProclamationCategory = Prisma.ProclamationCategoryModel
|
||||
/**
|
||||
* Model ProclamationCategorySubscriptionLevel
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type ProclamationCategorySubscriptionLevel =
|
||||
Prisma.ProclamationCategorySubscriptionLevelModel;
|
||||
export type ProclamationCategorySubscriptionLevel = Prisma.ProclamationCategorySubscriptionLevelModel
|
||||
/**
|
||||
* Model LicenseKey
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type LicenseKey = Prisma.LicenseKeyModel;
|
||||
export type LicenseKey = Prisma.LicenseKeyModel
|
||||
/**
|
||||
* Model ApiKey
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type ApiKey = Prisma.ApiKeyModel;
|
||||
export type ApiKey = Prisma.ApiKeyModel
|
||||
/**
|
||||
* Model SubscriptionLevel
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type SubscriptionLevel = Prisma.SubscriptionLevelModel;
|
||||
export type SubscriptionLevel = Prisma.SubscriptionLevelModel
|
||||
/**
|
||||
* Model ColorPalette
|
||||
*
|
||||
*/
|
||||
export type ColorPalette = Prisma.ColorPaletteModel
|
||||
|
||||
@@ -1,402 +1,481 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file exports various common sort, input & filter types that are not directly linked to a particular model.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
import type * as runtime from '@prisma/client/runtime/client';
|
||||
import * as $Enums from './enums.ts';
|
||||
import type * as Prisma from './internal/prismaNamespace.ts';
|
||||
import type * as runtime from "@prisma/client/runtime/client"
|
||||
import * as $Enums from "./enums.ts"
|
||||
import type * as Prisma from "./internal/prismaNamespace.ts"
|
||||
|
||||
|
||||
export type StringFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>;
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>;
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
mode?: Prisma.QueryMode;
|
||||
not?: Prisma.NestedStringFilter<$PrismaModel> | string;
|
||||
};
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
mode?: Prisma.QueryMode
|
||||
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
||||
}
|
||||
|
||||
export type BoolFilter<$PrismaModel = never> = {
|
||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean;
|
||||
};
|
||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
|
||||
}
|
||||
|
||||
export type StringNullableFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null;
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null;
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null;
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
mode?: Prisma.QueryMode;
|
||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null;
|
||||
};
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
mode?: Prisma.QueryMode
|
||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||
}
|
||||
|
||||
export type DateTimeFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>;
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>;
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string;
|
||||
};
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
|
||||
}
|
||||
|
||||
export type IntNullableFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null;
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null;
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null;
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null;
|
||||
};
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
||||
}
|
||||
|
||||
export type SortOrderInput = {
|
||||
sort: Prisma.SortOrder;
|
||||
nulls?: Prisma.NullsOrder;
|
||||
};
|
||||
sort: Prisma.SortOrder
|
||||
nulls?: Prisma.NullsOrder
|
||||
}
|
||||
|
||||
export type StringWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>;
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>;
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
mode?: Prisma.QueryMode;
|
||||
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string;
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedStringFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedStringFilter<$PrismaModel>;
|
||||
};
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
mode?: Prisma.QueryMode
|
||||
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type BoolWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean;
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedBoolFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedBoolFilter<$PrismaModel>;
|
||||
};
|
||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null;
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null;
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null;
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
mode?: Prisma.QueryMode;
|
||||
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null;
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>;
|
||||
};
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
mode?: Prisma.QueryMode
|
||||
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>;
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>;
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string;
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>;
|
||||
};
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type IntNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null;
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null;
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null;
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null;
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>;
|
||||
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||
};
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
||||
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type DateTimeNullableFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null;
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null;
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null;
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null;
|
||||
};
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
||||
}
|
||||
|
||||
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null;
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null;
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null;
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null;
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>;
|
||||
};
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type IntFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>;
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>;
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedIntFilter<$PrismaModel> | number;
|
||||
};
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
||||
}
|
||||
|
||||
export type IntWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>;
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>;
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number;
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
_avg?: Prisma.NestedFloatFilter<$PrismaModel>;
|
||||
_sum?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
};
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||
_sum?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type JsonFilter<$PrismaModel = never> =
|
||||
| Prisma.PatchUndefined<
|
||||
Prisma.Either<Required<JsonFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonFilterBase<$PrismaModel>>, 'path'>>,
|
||||
Required<JsonFilterBase<$PrismaModel>>
|
||||
>
|
||||
| Prisma.OptionalFlat<Omit<Required<JsonFilterBase<$PrismaModel>>, 'path'>>
|
||||
|
||||
export type JsonFilterBase<$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 JsonWithAggregatesFilter<$PrismaModel = never> =
|
||||
| Prisma.PatchUndefined<
|
||||
Prisma.Either<Required<JsonWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonWithAggregatesFilterBase<$PrismaModel>>, 'path'>>,
|
||||
Required<JsonWithAggregatesFilterBase<$PrismaModel>>
|
||||
>
|
||||
| Prisma.OptionalFlat<Omit<Required<JsonWithAggregatesFilterBase<$PrismaModel>>, 'path'>>
|
||||
|
||||
export type JsonWithAggregatesFilterBase<$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.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedJsonFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedJsonFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedStringFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>;
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>;
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedStringFilter<$PrismaModel> | string;
|
||||
};
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
||||
}
|
||||
|
||||
export type NestedBoolFilter<$PrismaModel = never> = {
|
||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean;
|
||||
};
|
||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
|
||||
}
|
||||
|
||||
export type NestedStringNullableFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null;
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null;
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null;
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null;
|
||||
};
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||
}
|
||||
|
||||
export type NestedDateTimeFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>;
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>;
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string;
|
||||
};
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
|
||||
}
|
||||
|
||||
export type NestedIntNullableFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null;
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null;
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null;
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null;
|
||||
};
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
||||
}
|
||||
|
||||
export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>;
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>;
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string;
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedStringFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedStringFilter<$PrismaModel>;
|
||||
};
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedIntFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>;
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>;
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedIntFilter<$PrismaModel> | number;
|
||||
};
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
||||
}
|
||||
|
||||
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean;
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedBoolFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedBoolFilter<$PrismaModel>;
|
||||
};
|
||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null;
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null;
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null;
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null;
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>;
|
||||
};
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>;
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>;
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string;
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>;
|
||||
};
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedIntNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null;
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null;
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null;
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null;
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>;
|
||||
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||
};
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
||||
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedFloatNullableFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null;
|
||||
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null;
|
||||
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null;
|
||||
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null;
|
||||
};
|
||||
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
|
||||
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
|
||||
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null
|
||||
}
|
||||
|
||||
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null;
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null;
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null;
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null;
|
||||
};
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
||||
}
|
||||
|
||||
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null;
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null;
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null;
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null;
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>;
|
||||
};
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>;
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>;
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number;
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
_avg?: Prisma.NestedFloatFilter<$PrismaModel>;
|
||||
_sum?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
};
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||
_sum?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedFloatFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>;
|
||||
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>;
|
||||
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedFloatFilter<$PrismaModel> | number;
|
||||
};
|
||||
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
|
||||
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
|
||||
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
|
||||
}
|
||||
|
||||
export type NestedJsonFilter<$PrismaModel = never> =
|
||||
| Prisma.PatchUndefined<
|
||||
Prisma.Either<Required<NestedJsonFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>,
|
||||
Required<NestedJsonFilterBase<$PrismaModel>>
|
||||
>
|
||||
| Prisma.OptionalFlat<Omit<Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>
|
||||
|
||||
export type NestedJsonFilterBase<$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
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file exports all enum related types from the schema.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
* This file exports all enum related types from the schema.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
// This file is empty because there are no enums in the schema.
|
||||
export {};
|
||||
export {}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,8 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* WARNING: This is an internal file that is subject to change!
|
||||
*
|
||||
@@ -14,266 +15,297 @@
|
||||
* model files in the `model` directory!
|
||||
*/
|
||||
|
||||
import * as runtime from '@prisma/client/runtime/index-browser';
|
||||
import * as runtime from "@prisma/client/runtime/index-browser"
|
||||
|
||||
export type * from '../models.ts';
|
||||
export type * from './prismaNamespace.ts';
|
||||
export type * from '../models.ts'
|
||||
export type * from './prismaNamespace.ts'
|
||||
|
||||
export const Decimal = runtime.Decimal
|
||||
|
||||
export const Decimal = runtime.Decimal;
|
||||
|
||||
export const NullTypes = {
|
||||
DbNull: runtime.NullTypes.DbNull as new (secret: never) => typeof runtime.DbNull,
|
||||
JsonNull: runtime.NullTypes.JsonNull as new (secret: never) => typeof runtime.JsonNull,
|
||||
AnyNull: runtime.NullTypes.AnyNull as new (secret: never) => typeof runtime.AnyNull
|
||||
};
|
||||
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
|
||||
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
|
||||
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
|
||||
}
|
||||
/**
|
||||
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
|
||||
*
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||
*/
|
||||
export const DbNull = runtime.DbNull;
|
||||
export const DbNull = runtime.DbNull
|
||||
|
||||
/**
|
||||
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
|
||||
*
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||
*/
|
||||
export const JsonNull = runtime.JsonNull;
|
||||
export const JsonNull = runtime.JsonNull
|
||||
|
||||
/**
|
||||
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
|
||||
*
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||
*/
|
||||
export const AnyNull = runtime.AnyNull;
|
||||
export const AnyNull = runtime.AnyNull
|
||||
|
||||
|
||||
export const ModelName = {
|
||||
User: 'User',
|
||||
Profile: 'Profile',
|
||||
Address: 'Address',
|
||||
Session: 'Session',
|
||||
Account: 'Account',
|
||||
UserRole: 'UserRole',
|
||||
Verification: 'Verification',
|
||||
Proclamation: 'Proclamation',
|
||||
UserProclamations: 'UserProclamations',
|
||||
SolfeggioFrequency: 'SolfeggioFrequency',
|
||||
ProclamationCategory: 'ProclamationCategory',
|
||||
ProclamationCategorySubscriptionLevel: 'ProclamationCategorySubscriptionLevel',
|
||||
LicenseKey: 'LicenseKey',
|
||||
ApiKey: 'ApiKey',
|
||||
SubscriptionLevel: 'SubscriptionLevel'
|
||||
} as const;
|
||||
User: 'User',
|
||||
Profile: 'Profile',
|
||||
Address: 'Address',
|
||||
Session: 'Session',
|
||||
Account: 'Account',
|
||||
UserRole: 'UserRole',
|
||||
Verification: 'Verification',
|
||||
Proclamation: 'Proclamation',
|
||||
UserProclamations: 'UserProclamations',
|
||||
SolfeggioFrequency: 'SolfeggioFrequency',
|
||||
ProclamationCategory: 'ProclamationCategory',
|
||||
ProclamationCategorySubscriptionLevel: 'ProclamationCategorySubscriptionLevel',
|
||||
LicenseKey: 'LicenseKey',
|
||||
ApiKey: 'ApiKey',
|
||||
SubscriptionLevel: 'SubscriptionLevel',
|
||||
ColorPalette: 'ColorPalette'
|
||||
} as const
|
||||
|
||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName];
|
||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||
|
||||
/*
|
||||
* Enums
|
||||
*/
|
||||
|
||||
export const TransactionIsolationLevel = runtime.makeStrictEnum({
|
||||
ReadUncommitted: 'ReadUncommitted',
|
||||
ReadCommitted: 'ReadCommitted',
|
||||
RepeatableRead: 'RepeatableRead',
|
||||
Serializable: 'Serializable'
|
||||
} as const);
|
||||
ReadUncommitted: 'ReadUncommitted',
|
||||
ReadCommitted: 'ReadCommitted',
|
||||
RepeatableRead: 'RepeatableRead',
|
||||
Serializable: 'Serializable'
|
||||
} as const)
|
||||
|
||||
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
|
||||
|
||||
export type TransactionIsolationLevel =
|
||||
(typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel];
|
||||
|
||||
export const UserScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
email: 'email',
|
||||
emailVerified: 'emailVerified',
|
||||
image: 'image',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
roleId: 'roleId'
|
||||
} as const;
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
email: 'email',
|
||||
emailVerified: 'emailVerified',
|
||||
image: 'image',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
roleId: 'roleId'
|
||||
} as const
|
||||
|
||||
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
|
||||
|
||||
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum];
|
||||
|
||||
export const ProfileScalarFieldEnum = {
|
||||
id: 'id',
|
||||
userId: 'userId',
|
||||
firstName: 'firstName',
|
||||
lastName: 'lastName',
|
||||
phoneNumber: 'phoneNumber',
|
||||
ghlContactId: 'ghlContactId'
|
||||
} as const;
|
||||
id: 'id',
|
||||
userId: 'userId',
|
||||
firstName: 'firstName',
|
||||
lastName: 'lastName',
|
||||
phoneNumber: 'phoneNumber',
|
||||
ghlContactId: 'ghlContactId'
|
||||
} as const
|
||||
|
||||
export type ProfileScalarFieldEnum = (typeof ProfileScalarFieldEnum)[keyof typeof ProfileScalarFieldEnum]
|
||||
|
||||
export type ProfileScalarFieldEnum =
|
||||
(typeof ProfileScalarFieldEnum)[keyof typeof ProfileScalarFieldEnum];
|
||||
|
||||
export const AddressScalarFieldEnum = {
|
||||
id: 'id',
|
||||
profileId: 'profileId',
|
||||
street: 'street',
|
||||
city: 'city',
|
||||
state: 'state',
|
||||
postalCode: 'postalCode',
|
||||
country: 'country',
|
||||
type: 'type',
|
||||
isDefaultMailing: 'isDefaultMailing',
|
||||
isDefaultBilling: 'isDefaultBilling',
|
||||
isDefaultShipping: 'isDefaultShipping'
|
||||
} as const;
|
||||
id: 'id',
|
||||
profileId: 'profileId',
|
||||
street: 'street',
|
||||
city: 'city',
|
||||
state: 'state',
|
||||
postalCode: 'postalCode',
|
||||
country: 'country',
|
||||
type: 'type',
|
||||
isDefaultMailing: 'isDefaultMailing',
|
||||
isDefaultBilling: 'isDefaultBilling',
|
||||
isDefaultShipping: 'isDefaultShipping'
|
||||
} as const
|
||||
|
||||
export type AddressScalarFieldEnum = (typeof AddressScalarFieldEnum)[keyof typeof AddressScalarFieldEnum]
|
||||
|
||||
export type AddressScalarFieldEnum =
|
||||
(typeof AddressScalarFieldEnum)[keyof typeof AddressScalarFieldEnum];
|
||||
|
||||
export const SessionScalarFieldEnum = {
|
||||
id: 'id',
|
||||
expiresAt: 'expiresAt',
|
||||
token: 'token',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
ipAddress: 'ipAddress',
|
||||
userAgent: 'userAgent',
|
||||
userId: 'userId'
|
||||
} as const;
|
||||
id: 'id',
|
||||
expiresAt: 'expiresAt',
|
||||
token: 'token',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
ipAddress: 'ipAddress',
|
||||
userAgent: 'userAgent',
|
||||
userId: 'userId'
|
||||
} as const
|
||||
|
||||
export type SessionScalarFieldEnum = (typeof SessionScalarFieldEnum)[keyof typeof SessionScalarFieldEnum]
|
||||
|
||||
export type SessionScalarFieldEnum =
|
||||
(typeof SessionScalarFieldEnum)[keyof typeof SessionScalarFieldEnum];
|
||||
|
||||
export const AccountScalarFieldEnum = {
|
||||
id: 'id',
|
||||
accountId: 'accountId',
|
||||
providerId: 'providerId',
|
||||
userId: 'userId',
|
||||
accessToken: 'accessToken',
|
||||
refreshToken: 'refreshToken',
|
||||
idToken: 'idToken',
|
||||
accessTokenExpiresAt: 'accessTokenExpiresAt',
|
||||
refreshTokenExpiresAt: 'refreshTokenExpiresAt',
|
||||
scope: 'scope',
|
||||
password: 'password',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const;
|
||||
id: 'id',
|
||||
accountId: 'accountId',
|
||||
providerId: 'providerId',
|
||||
userId: 'userId',
|
||||
accessToken: 'accessToken',
|
||||
refreshToken: 'refreshToken',
|
||||
idToken: 'idToken',
|
||||
accessTokenExpiresAt: 'accessTokenExpiresAt',
|
||||
refreshTokenExpiresAt: 'refreshTokenExpiresAt',
|
||||
scope: 'scope',
|
||||
password: 'password',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
|
||||
export type AccountScalarFieldEnum = (typeof AccountScalarFieldEnum)[keyof typeof AccountScalarFieldEnum]
|
||||
|
||||
export type AccountScalarFieldEnum =
|
||||
(typeof AccountScalarFieldEnum)[keyof typeof AccountScalarFieldEnum];
|
||||
|
||||
export const UserRoleScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name'
|
||||
} as const;
|
||||
id: 'id',
|
||||
name: 'name'
|
||||
} as const
|
||||
|
||||
export type UserRoleScalarFieldEnum = (typeof UserRoleScalarFieldEnum)[keyof typeof UserRoleScalarFieldEnum]
|
||||
|
||||
export type UserRoleScalarFieldEnum =
|
||||
(typeof UserRoleScalarFieldEnum)[keyof typeof UserRoleScalarFieldEnum];
|
||||
|
||||
export const VerificationScalarFieldEnum = {
|
||||
id: 'id',
|
||||
identifier: 'identifier',
|
||||
value: 'value',
|
||||
expiresAt: 'expiresAt',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const;
|
||||
id: 'id',
|
||||
identifier: 'identifier',
|
||||
value: 'value',
|
||||
expiresAt: 'expiresAt',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
|
||||
export type VerificationScalarFieldEnum = (typeof VerificationScalarFieldEnum)[keyof typeof VerificationScalarFieldEnum]
|
||||
|
||||
export type VerificationScalarFieldEnum =
|
||||
(typeof VerificationScalarFieldEnum)[keyof typeof VerificationScalarFieldEnum];
|
||||
|
||||
export const ProclamationScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
content: 'content',
|
||||
solfeggioFrequencyId: 'solfeggioFrequencyId',
|
||||
categoryId: 'categoryId',
|
||||
subscriptionLevelId: 'subscriptionLevelId'
|
||||
} as const;
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
content: 'content',
|
||||
solfeggioFrequencyId: 'solfeggioFrequencyId',
|
||||
categoryId: 'categoryId',
|
||||
subscriptionLevelId: 'subscriptionLevelId'
|
||||
} as const
|
||||
|
||||
export type ProclamationScalarFieldEnum = (typeof ProclamationScalarFieldEnum)[keyof typeof ProclamationScalarFieldEnum]
|
||||
|
||||
export type ProclamationScalarFieldEnum =
|
||||
(typeof ProclamationScalarFieldEnum)[keyof typeof ProclamationScalarFieldEnum];
|
||||
|
||||
export const UserProclamationsScalarFieldEnum = {
|
||||
userId: 'userId',
|
||||
proclamationId: 'proclamationId',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const;
|
||||
userId: 'userId',
|
||||
proclamationId: 'proclamationId',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
|
||||
export type UserProclamationsScalarFieldEnum = (typeof UserProclamationsScalarFieldEnum)[keyof typeof UserProclamationsScalarFieldEnum]
|
||||
|
||||
export type UserProclamationsScalarFieldEnum =
|
||||
(typeof UserProclamationsScalarFieldEnum)[keyof typeof UserProclamationsScalarFieldEnum];
|
||||
|
||||
export const SolfeggioFrequencyScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
value: 'value'
|
||||
} as const;
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
value: 'value'
|
||||
} as const
|
||||
|
||||
export type SolfeggioFrequencyScalarFieldEnum = (typeof SolfeggioFrequencyScalarFieldEnum)[keyof typeof SolfeggioFrequencyScalarFieldEnum]
|
||||
|
||||
export type SolfeggioFrequencyScalarFieldEnum =
|
||||
(typeof SolfeggioFrequencyScalarFieldEnum)[keyof typeof SolfeggioFrequencyScalarFieldEnum];
|
||||
|
||||
export const ProclamationCategoryScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
sortOrder: 'sortOrder',
|
||||
parentId: 'parentId'
|
||||
} as const;
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
sortOrder: 'sortOrder',
|
||||
parentId: 'parentId'
|
||||
} as const
|
||||
|
||||
export type ProclamationCategoryScalarFieldEnum = (typeof ProclamationCategoryScalarFieldEnum)[keyof typeof ProclamationCategoryScalarFieldEnum]
|
||||
|
||||
export type ProclamationCategoryScalarFieldEnum =
|
||||
(typeof ProclamationCategoryScalarFieldEnum)[keyof typeof ProclamationCategoryScalarFieldEnum];
|
||||
|
||||
export const ProclamationCategorySubscriptionLevelScalarFieldEnum = {
|
||||
categoryId: 'categoryId',
|
||||
subscriptionLevelId: 'subscriptionLevelId'
|
||||
} as const;
|
||||
categoryId: 'categoryId',
|
||||
subscriptionLevelId: 'subscriptionLevelId'
|
||||
} as const
|
||||
|
||||
export type ProclamationCategorySubscriptionLevelScalarFieldEnum = (typeof ProclamationCategorySubscriptionLevelScalarFieldEnum)[keyof typeof ProclamationCategorySubscriptionLevelScalarFieldEnum]
|
||||
|
||||
export type ProclamationCategorySubscriptionLevelScalarFieldEnum =
|
||||
(typeof ProclamationCategorySubscriptionLevelScalarFieldEnum)[keyof typeof ProclamationCategorySubscriptionLevelScalarFieldEnum];
|
||||
|
||||
export const LicenseKeyScalarFieldEnum = {
|
||||
id: 'id',
|
||||
key: 'key',
|
||||
active: 'active',
|
||||
level: 'level',
|
||||
userId: 'userId'
|
||||
} as const;
|
||||
id: 'id',
|
||||
key: 'key',
|
||||
active: 'active',
|
||||
level: 'level',
|
||||
userId: 'userId'
|
||||
} as const
|
||||
|
||||
export type LicenseKeyScalarFieldEnum = (typeof LicenseKeyScalarFieldEnum)[keyof typeof LicenseKeyScalarFieldEnum]
|
||||
|
||||
export type LicenseKeyScalarFieldEnum =
|
||||
(typeof LicenseKeyScalarFieldEnum)[keyof typeof LicenseKeyScalarFieldEnum];
|
||||
|
||||
export const ApiKeyScalarFieldEnum = {
|
||||
id: 'id',
|
||||
keyPrefix: 'keyPrefix',
|
||||
keyHash: 'keyHash',
|
||||
createdAt: 'createdAt',
|
||||
expiresAt: 'expiresAt',
|
||||
revokedAt: 'revokedAt'
|
||||
} as const;
|
||||
id: 'id',
|
||||
keyPrefix: 'keyPrefix',
|
||||
keyHash: 'keyHash',
|
||||
createdAt: 'createdAt',
|
||||
expiresAt: 'expiresAt',
|
||||
revokedAt: 'revokedAt'
|
||||
} as const
|
||||
|
||||
export type ApiKeyScalarFieldEnum = (typeof ApiKeyScalarFieldEnum)[keyof typeof ApiKeyScalarFieldEnum]
|
||||
|
||||
export type ApiKeyScalarFieldEnum =
|
||||
(typeof ApiKeyScalarFieldEnum)[keyof typeof ApiKeyScalarFieldEnum];
|
||||
|
||||
export const SubscriptionLevelScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
description: 'description'
|
||||
} as const;
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
description: 'description'
|
||||
} as const
|
||||
|
||||
export type SubscriptionLevelScalarFieldEnum = (typeof SubscriptionLevelScalarFieldEnum)[keyof typeof SubscriptionLevelScalarFieldEnum]
|
||||
|
||||
|
||||
export const ColorPaletteScalarFieldEnum = {
|
||||
solfeggioFrequencyId: 'solfeggioFrequencyId',
|
||||
colors: 'colors'
|
||||
} as const
|
||||
|
||||
export type ColorPaletteScalarFieldEnum = (typeof ColorPaletteScalarFieldEnum)[keyof typeof ColorPaletteScalarFieldEnum]
|
||||
|
||||
export type SubscriptionLevelScalarFieldEnum =
|
||||
(typeof SubscriptionLevelScalarFieldEnum)[keyof typeof SubscriptionLevelScalarFieldEnum];
|
||||
|
||||
export const SortOrder = {
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
} as const;
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
} as const
|
||||
|
||||
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
|
||||
|
||||
|
||||
export const JsonNullValueInput = {
|
||||
JsonNull: JsonNull
|
||||
} as const
|
||||
|
||||
export type JsonNullValueInput = (typeof JsonNullValueInput)[keyof typeof JsonNullValueInput]
|
||||
|
||||
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder];
|
||||
|
||||
export const QueryMode = {
|
||||
default: 'default',
|
||||
insensitive: 'insensitive'
|
||||
} as const;
|
||||
default: 'default',
|
||||
insensitive: 'insensitive'
|
||||
} as const
|
||||
|
||||
export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode]
|
||||
|
||||
export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode];
|
||||
|
||||
export const NullsOrder = {
|
||||
first: 'first',
|
||||
last: 'last'
|
||||
} as const;
|
||||
first: 'first',
|
||||
last: 'last'
|
||||
} as const
|
||||
|
||||
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
|
||||
|
||||
|
||||
export const JsonNullValueFilter = {
|
||||
DbNull: DbNull,
|
||||
JsonNull: JsonNull,
|
||||
AnyNull: AnyNull
|
||||
} as const
|
||||
|
||||
export type JsonNullValueFilter = (typeof JsonNullValueFilter)[keyof typeof JsonNullValueFilter]
|
||||
|
||||
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder];
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This is a barrel export file for all models and their related types.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
export type * from './models/User.ts';
|
||||
export type * from './models/Profile.ts';
|
||||
export type * from './models/Address.ts';
|
||||
export type * from './models/Session.ts';
|
||||
export type * from './models/Account.ts';
|
||||
export type * from './models/UserRole.ts';
|
||||
export type * from './models/Verification.ts';
|
||||
export type * from './models/Proclamation.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 './commonInputTypes.ts';
|
||||
export type * from './models/User.ts'
|
||||
export type * from './models/Profile.ts'
|
||||
export type * from './models/Address.ts'
|
||||
export type * from './models/Session.ts'
|
||||
export type * from './models/Account.ts'
|
||||
export type * from './models/UserRole.ts'
|
||||
export type * from './models/Verification.ts'
|
||||
export type * from './models/Proclamation.ts'
|
||||
export type * from './models/UserProclamations.ts'
|
||||
export type * from './models/SolfeggioFrequency.ts'
|
||||
export type * from './models/ProclamationCategory.ts'
|
||||
export type * from './models/ProclamationCategorySubscriptionLevel.ts'
|
||||
export type * from './models/LicenseKey.ts'
|
||||
export type * from './models/ApiKey.ts'
|
||||
export type * from './models/SubscriptionLevel.ts'
|
||||
export type * from './models/ColorPalette.ts'
|
||||
export type * from './commonInputTypes.ts'
|
||||
+1426
-1700
File diff suppressed because it is too large
Load Diff
+1354
-1618
File diff suppressed because it is too large
Load Diff
+1012
-1179
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1240
-1513
File diff suppressed because it is too large
Load Diff
+1249
-1503
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
+1874
-2298
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1062
-1300
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,64 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export const MAX_JSON_BODY_BYTES = 64 * 1024;
|
||||
|
||||
export function jsonOk(data: unknown, status = 200): Response {
|
||||
return new Response(JSON.stringify(data), {
|
||||
status,
|
||||
headers: { 'content-type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
export function jsonError(
|
||||
status: number,
|
||||
message: string,
|
||||
headers?: Record<string, string>
|
||||
): Response {
|
||||
return new Response(JSON.stringify({ message }), {
|
||||
status,
|
||||
headers: { 'content-type': 'application/json', ...headers }
|
||||
});
|
||||
}
|
||||
|
||||
/** Read and parse a JSON request body with a size cap. Throws 413 for
|
||||
* oversized bodies and 400 for invalid JSON / wrong content type. */
|
||||
export async function readJsonBody(request: Request): Promise<unknown> {
|
||||
const contentType = request.headers.get('content-type') ?? '';
|
||||
if (!contentType.includes('application/json')) {
|
||||
throw error(400, 'content-type must be application/json');
|
||||
}
|
||||
|
||||
const contentLength = Number(request.headers.get('content-length') ?? 0);
|
||||
if (contentLength > MAX_JSON_BODY_BYTES) {
|
||||
throw error(413, 'request body too large');
|
||||
}
|
||||
|
||||
const raw = await request.text();
|
||||
if (raw.length === 0) {
|
||||
throw error(400, 'request body is required');
|
||||
}
|
||||
if (raw.length > MAX_JSON_BODY_BYTES) {
|
||||
throw error(413, 'request body too large');
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
throw error(400, 'invalid JSON body');
|
||||
}
|
||||
}
|
||||
|
||||
/** Build a 429 response with Retry-After set in seconds. */
|
||||
export function rateLimited(retryAfterMs: number): Response {
|
||||
const retryAfterSeconds = Math.max(1, Math.ceil(retryAfterMs / 1000));
|
||||
return jsonError(429, 'rate limit exceeded', { 'retry-after': String(retryAfterSeconds) });
|
||||
}
|
||||
|
||||
export function clientIp(request: Request): string {
|
||||
const fwd = request.headers.get('x-forwarded-for');
|
||||
if (fwd) {
|
||||
const first = fwd.split(',')[0]?.trim();
|
||||
if (first) return first;
|
||||
}
|
||||
return request.headers.get('x-real-ip') ?? 'unknown';
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { rateLimit, _clearRateLimits } from './v2-rate-limit';
|
||||
|
||||
describe('rateLimit', () => {
|
||||
beforeEach(() => {
|
||||
_clearRateLimits();
|
||||
});
|
||||
|
||||
it('allows requests up to the limit', () => {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const r = rateLimit({ key: 'k', limit: 3, windowMs: 60_000, now: 1000 });
|
||||
expect(r.ok).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects once the limit is exceeded and reports Retry-After', () => {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
rateLimit({ key: 'k', limit: 3, windowMs: 60_000, now: 1000 });
|
||||
}
|
||||
const r = rateLimit({ key: 'k', limit: 3, windowMs: 60_000, now: 1000 });
|
||||
expect(r.ok).toBe(false);
|
||||
expect(r.retryAfterMs).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('treats keys independently', () => {
|
||||
rateLimit({ key: 'a', limit: 1, windowMs: 60_000, now: 1000 });
|
||||
expect(rateLimit({ key: 'a', limit: 1, windowMs: 60_000, now: 1000 }).ok).toBe(false);
|
||||
expect(rateLimit({ key: 'b', limit: 1, windowMs: 60_000, now: 1000 }).ok).toBe(true);
|
||||
});
|
||||
|
||||
it('lets the window expire before allowing again', () => {
|
||||
for (let i = 0; i < 2; i++) {
|
||||
rateLimit({ key: 'k', limit: 2, windowMs: 10_000, now: 1000 });
|
||||
}
|
||||
expect(rateLimit({ key: 'k', limit: 2, windowMs: 10_000, now: 1000 }).ok).toBe(false);
|
||||
expect(rateLimit({ key: 'k', limit: 2, windowMs: 10_000, now: 1000 + 10_000 + 1 }).ok).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
// Simple in-memory sliding-window rate limiter, keyed per process. Suitable
|
||||
// for per-IP / per-email limits on v2 routes; for multi-instance deployments
|
||||
// this should move to shared storage (e.g. the database or Redis).
|
||||
|
||||
type Bucket = { timestamps: number[] };
|
||||
|
||||
const buckets = new Map<string, Bucket>();
|
||||
|
||||
export function rateLimit(opts: { key: string; limit: number; windowMs: number; now?: number }): {
|
||||
ok: boolean;
|
||||
retryAfterMs: number;
|
||||
} {
|
||||
const now = opts.now ?? Date.now();
|
||||
|
||||
let bucket = buckets.get(opts.key);
|
||||
if (!bucket) {
|
||||
bucket = { timestamps: [] };
|
||||
buckets.set(opts.key, bucket);
|
||||
}
|
||||
|
||||
bucket.timestamps = bucket.timestamps.filter((t) => now - t < opts.windowMs);
|
||||
|
||||
if (bucket.timestamps.length >= opts.limit) {
|
||||
const oldest = bucket.timestamps[0];
|
||||
const retryAfterMs = Math.max(0, oldest + opts.windowMs - now);
|
||||
return { ok: false, retryAfterMs };
|
||||
}
|
||||
|
||||
bucket.timestamps.push(now);
|
||||
return { ok: true, retryAfterMs: 0 };
|
||||
}
|
||||
|
||||
/** Test hook: clears all rate-limit buckets. */
|
||||
export function _clearRateLimits(): void {
|
||||
buckets.clear();
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
CANONICAL_FREQUENCIES,
|
||||
CANONICAL_PALETTES,
|
||||
canonicalIdForFrequency,
|
||||
serializeCategory,
|
||||
serializeColorPalette,
|
||||
serializeLicense,
|
||||
serializeProclamation,
|
||||
serializeSubscriptionLevel
|
||||
} from './v2-serializers';
|
||||
|
||||
describe('canonicalIdForFrequency', () => {
|
||||
it('maps Hz values to canonical ids via the kiosk frequencyIdForHz map', () => {
|
||||
expect(canonicalIdForFrequency({ value: 174 })).toBe(1);
|
||||
expect(canonicalIdForFrequency({ value: 285 })).toBe(1);
|
||||
expect(canonicalIdForFrequency({ value: 396 })).toBe(2);
|
||||
expect(canonicalIdForFrequency({ value: 417 })).toBe(3);
|
||||
expect(canonicalIdForFrequency({ value: 432 })).toBe(4);
|
||||
expect(canonicalIdForFrequency({ value: 528 })).toBe(5);
|
||||
expect(canonicalIdForFrequency({ value: 639 })).toBe(6);
|
||||
expect(canonicalIdForFrequency({ value: 741 })).toBe(7);
|
||||
expect(canonicalIdForFrequency({ value: 852 })).toBe(8);
|
||||
expect(canonicalIdForFrequency({ value: 963 })).toBe(9);
|
||||
});
|
||||
|
||||
it('returns null for frequencies with no canonical id (e.g. 936 Hz)', () => {
|
||||
expect(canonicalIdForFrequency({ value: 936 })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CANONICAL_FREQUENCIES', () => {
|
||||
it('emits exactly the 9 canonical frequencies with non-zero ids', () => {
|
||||
expect(CANONICAL_FREQUENCIES).toHaveLength(9);
|
||||
for (const freq of CANONICAL_FREQUENCIES) {
|
||||
expect(freq.id).toBeGreaterThan(0);
|
||||
expect(typeof freq.name).toBe('string');
|
||||
expect(canonicalIdForFrequency({ value: freq.value })).toBe(freq.id);
|
||||
}
|
||||
});
|
||||
|
||||
it('ships a 7-color palette for every canonical id', () => {
|
||||
for (const freq of CANONICAL_FREQUENCIES) {
|
||||
const colors = CANONICAL_PALETTES[freq.id];
|
||||
expect(colors).toBeDefined();
|
||||
expect(colors).toHaveLength(7);
|
||||
for (const c of colors) {
|
||||
expect(c).toMatch(/^#([0-9A-Fa-f]{6})$/);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('serializeProclamation', () => {
|
||||
const root396 = { id: 7, name: 'Root', value: 396 };
|
||||
const crown936 = { id: 1, name: 'Crown', value: 936 };
|
||||
const solarPlexus528 = { id: 5, name: 'Solar Plexus', value: 528 };
|
||||
|
||||
const base = {
|
||||
id: 'b40fdcb9-703c-420e-af32-a081964c6f08',
|
||||
name: 'Peace & Relaxation',
|
||||
content: '<p>The text content, HTML formatted</p>',
|
||||
categoryId: 5,
|
||||
subscriptionLevelId: 1
|
||||
};
|
||||
|
||||
it('derives solfeggioFrequencyId from the Hz value, not the DB id', () => {
|
||||
// DB row id 7 is 396 Hz -> canonical id 2.
|
||||
const record = serializeProclamation({
|
||||
...base,
|
||||
solfeggioFrequency: root396
|
||||
});
|
||||
expect(record).toEqual({
|
||||
...base,
|
||||
solfeggioFrequencyId: 2
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null and drops proclamations whose frequency has no canonical id (936 Hz)', () => {
|
||||
const record = serializeProclamation({
|
||||
...base,
|
||||
solfeggioFrequency: crown936
|
||||
});
|
||||
expect(record).toBeNull();
|
||||
});
|
||||
|
||||
it('passes through a null subscriptionLevelId (kiosk treats as level 1)', () => {
|
||||
const record = serializeProclamation({
|
||||
...base,
|
||||
subscriptionLevelId: null,
|
||||
solfeggioFrequency: solarPlexus528
|
||||
});
|
||||
expect(record?.subscriptionLevelId).toBeNull();
|
||||
});
|
||||
|
||||
it('embeds a 7-color palette when provided', () => {
|
||||
const palette = ['#32CD32', '#3CB371', '#90EE90', '#98FB98', '#ADFFA9', '#C8FFD2', '#F0FFF0'];
|
||||
const record = serializeProclamation(
|
||||
{
|
||||
...base,
|
||||
solfeggioFrequency: solarPlexus528
|
||||
},
|
||||
palette
|
||||
);
|
||||
expect(record?.colorPalette).toEqual(palette);
|
||||
});
|
||||
|
||||
it('omits colorPalette when not provided', () => {
|
||||
const record = serializeProclamation({
|
||||
...base,
|
||||
solfeggioFrequency: solarPlexus528
|
||||
});
|
||||
expect(record?.colorPalette).toBeUndefined();
|
||||
expect(Object.keys(record ?? {})).not.toContain('colorPalette');
|
||||
});
|
||||
});
|
||||
|
||||
describe('serializeCategory', () => {
|
||||
it('keeps parentId null for root categories', () => {
|
||||
expect(serializeCategory({ id: 4, name: 'Spiritual', sortOrder: 0, parentId: null })).toEqual({
|
||||
id: 4,
|
||||
name: 'Spiritual',
|
||||
sortOrder: 0,
|
||||
parentId: null
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps parentId int for children', () => {
|
||||
expect(serializeCategory({ id: 5, name: 'General', sortOrder: 0, parentId: 4 })).toEqual({
|
||||
id: 5,
|
||||
name: 'General',
|
||||
sortOrder: 0,
|
||||
parentId: 4
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('serializeLicense', () => {
|
||||
it('returns subscription_level as an object when the level row exists', () => {
|
||||
const result = serializeLicense({
|
||||
key: 'XXXX-XXXX-XXXX-XXXX',
|
||||
emailAddress: 'user@example.com',
|
||||
levelName: 'Advanced',
|
||||
level: { id: 2, name: 'Advanced', description: 'General + Specific' }
|
||||
});
|
||||
expect(result).toEqual({
|
||||
key: 'XXXX-XXXX-XXXX-XXXX',
|
||||
email_address: 'user@example.com',
|
||||
subscription_level: { id: 2, name: 'Advanced', description: 'General + Specific' }
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to a structurally consistent object when no level row matches', () => {
|
||||
const result = serializeLicense({
|
||||
key: 'XXXX-XXXX-XXXX-XXXX',
|
||||
emailAddress: 'user@example.com',
|
||||
levelName: 'SomeCustomLevel',
|
||||
level: null
|
||||
});
|
||||
expect(result).toEqual({
|
||||
key: 'XXXX-XXXX-XXXX-XXXX',
|
||||
email_address: 'user@example.com',
|
||||
subscription_level: { id: null, name: 'SomeCustomLevel', description: null }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('serializeSubscriptionLevel', () => {
|
||||
it('maps rows to {id, name, description}', () => {
|
||||
expect(
|
||||
serializeSubscriptionLevel({ id: 1, name: 'Basic', description: 'General only' })
|
||||
).toEqual({
|
||||
id: 1,
|
||||
name: 'Basic',
|
||||
description: 'General only'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('serializeColorPalette', () => {
|
||||
it('maps rows to {solfeggioFrequencyId, colors}', () => {
|
||||
const colors = ['#8B6914', '#B8860B', '#DAA520', '#E8B64C', '#FFD700', '#FFE9A8', '#FFF4CC'];
|
||||
expect(serializeColorPalette({ solfeggioFrequencyId: 1, colors })).toEqual({
|
||||
solfeggioFrequencyId: 1,
|
||||
colors
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
// Canonical solfeggio id <- Hz, mirroring the kiosk's `frequencyIdForHz()`
|
||||
// map (radiantwave-rebuild src/audio/audio_downloader.cpp). v2 must emit these
|
||||
// canonical ids (1-9), never the portal DB autoincrement ids, because
|
||||
// proclamations and color palettes on the kiosk are keyed by them.
|
||||
export 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
|
||||
};
|
||||
|
||||
// The 9 canonical frequencies exactly as the kiosk expects from
|
||||
// GET /frequencies (id 1-9, non-zero, `value` recognized by frequencyIdForHz).
|
||||
export const CANONICAL_FREQUENCIES: { id: number; name: string; value: number }[] = [
|
||||
{ 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 }
|
||||
];
|
||||
|
||||
// Seed palettes per canonical id, matching the kiosk's
|
||||
// DatabaseManager::seedColorPalettes(). Used to backfill /color-palettes
|
||||
// when the DB has not been seeded for an id yet.
|
||||
export const CANONICAL_PALETTES: Record<number, string[]> = {
|
||||
1: ['#8B6914', '#B8860B', '#DAA520', '#E8B64C', '#FFD700', '#FFE9A8', '#FFF4CC'],
|
||||
2: ['#8B0000', '#B22222', '#DC143C', '#FF6347', '#FF8C69', '#FFA500', '#FFDAB9'],
|
||||
3: ['#FF4500', '#FF7F50', '#FF8C00', '#FFA500', '#FFC87F', '#FFDFBA', '#FFEBCD'],
|
||||
4: ['#8B2252', '#CD3278', '#DB7093', '#FF69B4', '#FFB6C1', '#FFC0CB', '#FFE4E1'],
|
||||
5: ['#32CD32', '#3CB371', '#90EE90', '#98FB98', '#ADFFA9', '#C8FFD2', '#F0FFF0'],
|
||||
6: ['#00FF7F', '#00CED1', '#40E0D0', '#5F9EA0', '#7FFFD4', '#AFEEEE', '#E0FFFF'],
|
||||
7: ['#00BFFF', '#1E90FF', '#4169E1', '#6495ED', '#87CEEB', '#ADD8E6', '#F0F8FF'],
|
||||
8: ['#4B0082', '#9370DB', '#8A2BE2', '#7B68EE', '#DA70D6', '#D8BFD8', '#E6E6FA'],
|
||||
9: ['#9400D3', '#BA55D3', '#C71585', '#DDA0DD', '#EE82EE', '#FFF0F5', '#FAF0FA']
|
||||
};
|
||||
|
||||
/** Map a SolfeggioFrequency row to its canonical id, or null if it has no
|
||||
* canonical id (e.g. 936 Hz). */
|
||||
export function canonicalIdForFrequency(freq: { value: number }): number | null {
|
||||
return CANONICAL_ID_FOR_HZ[freq.value] ?? null;
|
||||
}
|
||||
|
||||
export type V2Proclamation = {
|
||||
id: string;
|
||||
name: string;
|
||||
content: string;
|
||||
categoryId: number;
|
||||
solfeggioFrequencyId: number;
|
||||
subscriptionLevelId: number | null;
|
||||
colorPalette?: string[];
|
||||
};
|
||||
|
||||
export type ProclamationWithFrequency = {
|
||||
id: string;
|
||||
name: string;
|
||||
content: string;
|
||||
categoryId: number;
|
||||
subscriptionLevelId: number | null;
|
||||
solfeggioFrequency: { value: number };
|
||||
};
|
||||
|
||||
/** Serialize a DB proclamation row to the v2 wire format. Returns null when
|
||||
* the proclamation's frequency has no canonical id (dropped from v2). */
|
||||
export function serializeProclamation(
|
||||
p: ProclamationWithFrequency,
|
||||
palette?: string[]
|
||||
): V2Proclamation | null {
|
||||
const solfeggioFrequencyId = CANONICAL_ID_FOR_HZ[p.solfeggioFrequency.value];
|
||||
if (!solfeggioFrequencyId) return null;
|
||||
|
||||
const record: V2Proclamation = {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
content: p.content,
|
||||
categoryId: p.categoryId,
|
||||
solfeggioFrequencyId,
|
||||
subscriptionLevelId: p.subscriptionLevelId
|
||||
};
|
||||
|
||||
if (palette && palette.length > 0) {
|
||||
record.colorPalette = palette;
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
export type V2Category = {
|
||||
id: number;
|
||||
name: string;
|
||||
sortOrder: number;
|
||||
parentId: number | null;
|
||||
};
|
||||
|
||||
export function serializeCategory(c: {
|
||||
id: number;
|
||||
name: string;
|
||||
sortOrder: number;
|
||||
parentId: number | null;
|
||||
}): V2Category {
|
||||
return { id: c.id, name: c.name, sortOrder: c.sortOrder, parentId: c.parentId };
|
||||
}
|
||||
|
||||
export type V2SubscriptionLevel = {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string | null;
|
||||
};
|
||||
|
||||
export function serializeSubscriptionLevel(level: {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string | null;
|
||||
}): V2SubscriptionLevel {
|
||||
return { id: level.id, name: level.name, description: level.description };
|
||||
}
|
||||
|
||||
export type V2License = {
|
||||
key: string;
|
||||
email_address: string;
|
||||
subscription_level: { id: number | null; name: string; description: string | null };
|
||||
};
|
||||
|
||||
/** Build the POST /license response. `subscription_level` is always an object;
|
||||
* when no SubscriptionLevel row matches the license's level name, fall back to
|
||||
* a structurally consistent object with a null id/description. */
|
||||
export function serializeLicense(opts: {
|
||||
key: string;
|
||||
emailAddress: string;
|
||||
levelName: string;
|
||||
level: { id: number; name: string; description: string | null } | null;
|
||||
}): V2License {
|
||||
return {
|
||||
key: opts.key,
|
||||
email_address: opts.emailAddress,
|
||||
subscription_level: opts.level
|
||||
? { id: opts.level.id, name: opts.level.name, description: opts.level.description }
|
||||
: { id: null, name: opts.levelName, description: null }
|
||||
};
|
||||
}
|
||||
|
||||
export type V2ColorPalette = {
|
||||
solfeggioFrequencyId: number;
|
||||
colors: string[];
|
||||
};
|
||||
|
||||
export function serializeColorPalette(palette: {
|
||||
solfeggioFrequencyId: number;
|
||||
colors: string[];
|
||||
}): V2ColorPalette {
|
||||
return { solfeggioFrequencyId: palette.solfeggioFrequencyId, colors: palette.colors };
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import {
|
||||
computeSignature,
|
||||
buildCanonicalRequest,
|
||||
verifyRequestSignature,
|
||||
_clearNonces
|
||||
} from './v2-signature';
|
||||
|
||||
const SECRET = 'test-onboarding-secret';
|
||||
|
||||
function signedRequest(opts: {
|
||||
now?: Date;
|
||||
timestampOffsetSec?: number;
|
||||
tamper?: Partial<Record<'signature' | 'nonce' | 'timestamp' | 'body', string>>;
|
||||
}) {
|
||||
const method = 'POST';
|
||||
const path = '/api/v2/automations/create/account';
|
||||
const timestamp = String(Math.floor((opts.now ?? new Date()).getTime() / 1000));
|
||||
const nonce = 'a48c0f2e-5f34-4b0b-9a12-3c7d6b8e1f00';
|
||||
const body = '{"email":"user@example.com","subscription_level":"Basic"}';
|
||||
|
||||
let ts = timestamp;
|
||||
let nonceValue = nonce;
|
||||
|
||||
if (opts.tamper?.timestamp) ts = opts.tamper.timestamp;
|
||||
if (opts.tamper?.nonce) nonceValue = opts.tamper.nonce;
|
||||
|
||||
let sig = computeSignature(SECRET, buildCanonicalRequest(method, path, ts, nonceValue, body));
|
||||
|
||||
if (opts.tamper?.body)
|
||||
sig = computeSignature(
|
||||
SECRET,
|
||||
buildCanonicalRequest(method, path, ts, nonceValue, opts.tamper.body)
|
||||
);
|
||||
if (opts.tamper?.signature) sig = opts.tamper.signature;
|
||||
|
||||
const headers = new Headers({
|
||||
'content-type': 'application/json',
|
||||
'x-timestamp': ts,
|
||||
'x-nonce': nonceValue,
|
||||
'x-signature': sig
|
||||
});
|
||||
|
||||
return { method, path, headers, rawBody: body };
|
||||
}
|
||||
|
||||
describe('verifyRequestSignature', () => {
|
||||
beforeEach(() => {
|
||||
_clearNonces();
|
||||
});
|
||||
|
||||
it('accepts a correctly signed request', () => {
|
||||
const { method, path, headers, rawBody } = signedRequest({});
|
||||
const result = verifyRequestSignature({ method, path, headers, rawBody, secret: SECRET });
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects a request with a wrong signature', () => {
|
||||
const { method, path, headers, rawBody } = signedRequest({
|
||||
tamper: { signature: 'deadbeef'.repeat(8) }
|
||||
});
|
||||
const result = verifyRequestSignature({ method, path, headers, rawBody, secret: SECRET });
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.reason).toBe('invalid signature');
|
||||
});
|
||||
|
||||
it('rejects a request signed with the wrong secret', () => {
|
||||
const { method, path, headers, rawBody } = signedRequest({});
|
||||
const result = verifyRequestSignature({
|
||||
method,
|
||||
path,
|
||||
headers,
|
||||
rawBody,
|
||||
secret: 'other-secret'
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects missing signature headers', () => {
|
||||
const { method, path, rawBody } = signedRequest({});
|
||||
const result = verifyRequestSignature({
|
||||
method,
|
||||
path,
|
||||
headers: new Headers(),
|
||||
rawBody,
|
||||
secret: SECRET
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects an expired timestamp (>300s skew)', () => {
|
||||
const now = new Date();
|
||||
const { method, path, headers, rawBody } = signedRequest({
|
||||
tamper: { timestamp: String(Math.floor(now.getTime() / 1000) - 600) }
|
||||
});
|
||||
const result = verifyRequestSignature({ method, path, headers, rawBody, secret: SECRET, now });
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.reason).toBe('expired timestamp');
|
||||
});
|
||||
|
||||
it('accepts a timestamp within the 300s skew', () => {
|
||||
const now = new Date();
|
||||
const { method, path, headers, rawBody } = signedRequest({
|
||||
tamper: { timestamp: String(Math.floor(now.getTime() / 1000) - 120) }
|
||||
});
|
||||
const result = verifyRequestSignature({ method, path, headers, rawBody, secret: SECRET, now });
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects a replayed nonce', () => {
|
||||
const { method, path, headers, rawBody } = signedRequest({});
|
||||
expect(verifyRequestSignature({ method, path, headers, rawBody, secret: SECRET }).ok).toBe(
|
||||
true
|
||||
);
|
||||
|
||||
const replay = verifyRequestSignature({ method, path, headers, rawBody, secret: SECRET });
|
||||
expect(replay.ok).toBe(false);
|
||||
if (!replay.ok) expect(replay.reason).toBe('replayed nonce');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import { createHmac, timingSafeEqual } from 'node:crypto';
|
||||
|
||||
export const SIGNATURE_MAX_SKEW_SECONDS = 300;
|
||||
|
||||
export function computeSignature(secret: string, canonicalRequest: string): string {
|
||||
return createHmac('sha256', secret).update(canonicalRequest, 'utf8').digest('hex');
|
||||
}
|
||||
|
||||
/** Canonical request string covered by the HMAC: method, path, timestamp,
|
||||
* nonce, and the raw body, newline-separated. */
|
||||
export function buildCanonicalRequest(
|
||||
method: string,
|
||||
path: string,
|
||||
timestamp: string,
|
||||
nonce: string,
|
||||
body: string
|
||||
): string {
|
||||
return [method, path, timestamp, nonce, body].join('\n');
|
||||
}
|
||||
|
||||
export function safeEqual(a: string, b: string): boolean {
|
||||
const bufA = Buffer.from(a, 'utf8');
|
||||
const bufB = Buffer.from(b, 'utf8');
|
||||
if (bufA.length !== bufB.length) return false;
|
||||
return timingSafeEqual(bufA, bufB);
|
||||
}
|
||||
|
||||
// In-memory nonce store. Prunes entries older than twice the allowed skew.
|
||||
const seenNonces = new Map<string, number>();
|
||||
|
||||
export function isNonceReplayed(nonce: string, now = Date.now()): boolean {
|
||||
const seenAt = seenNonces.get(nonce);
|
||||
if (seenAt === undefined) return false;
|
||||
// Keep treating it as replayed only while within the replay window; beyond
|
||||
// that the entry is pruned and the nonce could in theory be reused, but the
|
||||
// timestamp skew check still bounds it.
|
||||
return now - seenAt < SIGNATURE_MAX_SKEW_SECONDS * 2 * 1000;
|
||||
}
|
||||
|
||||
export function recordNonce(nonce: string, now = Date.now()): void {
|
||||
seenNonces.set(nonce, now);
|
||||
// Opportunistically prune stale entries.
|
||||
const cutoff = now - SIGNATURE_MAX_SKEW_SECONDS * 2 * 1000;
|
||||
for (const [key, seenAt] of seenNonces) {
|
||||
if (seenAt < cutoff) seenNonces.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
export type SignatureVerification = { ok: true } | { ok: false; reason: string };
|
||||
|
||||
export function verifyRequestSignature(opts: {
|
||||
method: string;
|
||||
path: string;
|
||||
headers: Headers;
|
||||
rawBody: string;
|
||||
secret: string;
|
||||
now?: Date;
|
||||
}): SignatureVerification {
|
||||
const { method, path, headers, rawBody, secret } = opts;
|
||||
|
||||
const timestamp = headers.get('x-timestamp');
|
||||
const nonce = headers.get('x-nonce');
|
||||
const signature = headers.get('x-signature');
|
||||
|
||||
if (!timestamp || !nonce || !signature) {
|
||||
return { ok: false, reason: 'missing signature headers' };
|
||||
}
|
||||
|
||||
const ts = Number(timestamp);
|
||||
if (!Number.isFinite(ts)) {
|
||||
return { ok: false, reason: 'invalid timestamp' };
|
||||
}
|
||||
|
||||
const now = (opts.now ?? new Date()).getTime() / 1000;
|
||||
if (Math.abs(now - ts) > SIGNATURE_MAX_SKEW_SECONDS) {
|
||||
return { ok: false, reason: 'expired timestamp' };
|
||||
}
|
||||
|
||||
if (isNonceReplayed(nonce)) {
|
||||
return { ok: false, reason: 'replayed nonce' };
|
||||
}
|
||||
|
||||
const canonical = buildCanonicalRequest(method, path, timestamp, nonce, rawBody);
|
||||
const expected = computeSignature(secret, canonical);
|
||||
if (!safeEqual(expected, signature)) {
|
||||
return { ok: false, reason: 'invalid signature' };
|
||||
}
|
||||
|
||||
recordNonce(nonce);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
/** Test hook: clears the in-memory nonce store. */
|
||||
export function _clearNonces(): void {
|
||||
seenNonces.clear();
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { json, error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { auth } from '$lib/auth';
|
||||
import { prisma } from '$lib/db';
|
||||
import { sendMail } from '$lib/server/resend';
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { clientIp, rateLimited } from '$lib/server/v2-helpers';
|
||||
import { rateLimit } from '$lib/server/v2-rate-limit';
|
||||
import { verifyRequestSignature } from '$lib/server/v2-signature';
|
||||
import { MAX_JSON_BODY_BYTES } from '$lib/server/v2-helpers';
|
||||
|
||||
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
function generateLicenseKey(): string {
|
||||
const hex = crypto.randomUUID().replace(/-/g, '').slice(0, 16).toUpperCase();
|
||||
return `${hex.slice(0, 4)}-${hex.slice(4, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}`;
|
||||
}
|
||||
|
||||
function unauthorized(): Response {
|
||||
return new Response(JSON.stringify({ error: 'invalid signature' }), {
|
||||
status: 401,
|
||||
headers: { 'content-type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
export const POST: RequestHandler = async ({ request, url }) => {
|
||||
const secret = env.API_V2_ONBOARDING_SECRET;
|
||||
if (!secret) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const rawBody = await request.text();
|
||||
if (rawBody.length === 0) {
|
||||
throw error(400, 'request body is required');
|
||||
}
|
||||
if (rawBody.length > MAX_JSON_BODY_BYTES) {
|
||||
throw error(413, 'request body too large');
|
||||
}
|
||||
|
||||
const verification = verifyRequestSignature({
|
||||
method: 'POST',
|
||||
path: url.pathname,
|
||||
headers: request.headers,
|
||||
rawBody,
|
||||
secret
|
||||
});
|
||||
if (!verification.ok) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const ip = clientIp(request);
|
||||
const ipLimit = rateLimit({ key: `onboarding:ip:${ip}`, limit: 5, windowMs: 60_000 });
|
||||
if (!ipLimit.ok) {
|
||||
return rateLimited(ipLimit.retryAfterMs);
|
||||
}
|
||||
|
||||
let body: unknown;
|
||||
try {
|
||||
body = JSON.parse(rawBody);
|
||||
} catch {
|
||||
throw error(400, 'invalid JSON body');
|
||||
}
|
||||
|
||||
const { email, name, subscription_level } = body as Record<string, unknown>;
|
||||
|
||||
if (typeof email !== 'string' || !email.trim()) {
|
||||
error(400, 'email is required');
|
||||
}
|
||||
const cleanEmail = email.trim().toLowerCase();
|
||||
if (cleanEmail.length > 254 || !EMAIL_RE.test(cleanEmail)) {
|
||||
error(400, 'email is required');
|
||||
}
|
||||
|
||||
if (typeof subscription_level !== 'string' || !subscription_level.trim()) {
|
||||
error(400, 'subscription_level is required');
|
||||
}
|
||||
|
||||
const emailLimit = rateLimit({
|
||||
key: `onboarding:email:${cleanEmail}`,
|
||||
limit: 3,
|
||||
windowMs: 24 * 60 * 60 * 1000
|
||||
});
|
||||
if (!emailLimit.ok) {
|
||||
return rateLimited(emailLimit.retryAfterMs);
|
||||
}
|
||||
|
||||
const levelName = subscription_level.trim();
|
||||
const level = await prisma.subscriptionLevel.findFirst({ where: { name: levelName } });
|
||||
if (!level) {
|
||||
error(400, `subscription_level "${levelName}" not found`);
|
||||
}
|
||||
|
||||
const displayName =
|
||||
typeof name === 'string' && name.trim() ? name.trim().slice(0, 120) : cleanEmail.split('@')[0];
|
||||
|
||||
let user = await prisma.user.findUnique({ where: { email: cleanEmail } });
|
||||
|
||||
if (!user) {
|
||||
const tempPassword = crypto.randomUUID().replace(/-/g, '').slice(0, 20);
|
||||
|
||||
try {
|
||||
await auth.api.signUpEmail({
|
||||
body: {
|
||||
email: cleanEmail,
|
||||
password: tempPassword,
|
||||
name: displayName
|
||||
}
|
||||
});
|
||||
|
||||
user = await prisma.user.findUnique({ where: { email: cleanEmail } });
|
||||
|
||||
const verification = await prisma.verification.findFirst({
|
||||
where: {
|
||||
identifier: cleanEmail,
|
||||
expiresAt: { gt: new Date() }
|
||||
},
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
|
||||
const verifyUrl = verification
|
||||
? `${env.BETTER_AUTH_URL}/api/auth/verify-email?token=${verification.value}&callbackURL=/login?verified=true`
|
||||
: `${env.BETTER_AUTH_URL}/login`;
|
||||
|
||||
await sendMail({
|
||||
to: cleanEmail,
|
||||
subject: 'Your account is ready',
|
||||
templateId: 'automated-user-registration',
|
||||
data: {
|
||||
firstName: displayName,
|
||||
tempPassword,
|
||||
verifyUrl,
|
||||
loginUrl: `${env.BETTER_AUTH_URL}/login`
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to create user:', err);
|
||||
error(500, 'Failed to create account');
|
||||
}
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
error(500, 'Failed to find or create user');
|
||||
}
|
||||
|
||||
const key = generateLicenseKey();
|
||||
|
||||
await prisma.licenseKey.create({
|
||||
data: {
|
||||
key,
|
||||
active: true,
|
||||
level: levelName,
|
||||
userId: user.id
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await sendMail({
|
||||
to: cleanEmail,
|
||||
subject: 'Your Rhemawave License Key',
|
||||
templateId: 'license-key-delivery',
|
||||
data: {
|
||||
firstName: displayName,
|
||||
license_key: key
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to send license key email:', e);
|
||||
}
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
message: 'Account and license key created',
|
||||
user_id: user.id
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,236 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { isHttpError } from '@sveltejs/kit';
|
||||
import { computeSignature, buildCanonicalRequest, _clearNonces } from '$lib/server/v2-signature';
|
||||
import { _clearRateLimits } from '$lib/server/v2-rate-limit';
|
||||
|
||||
const { authApi, sendMailMock } = vi.hoisted(() => ({
|
||||
authApi: { signUpEmail: vi.fn() },
|
||||
sendMailMock: vi.fn()
|
||||
}));
|
||||
|
||||
vi.mock('$env/dynamic/private', () => ({
|
||||
env: {
|
||||
API_V2_ONBOARDING_SECRET: 'test-onboarding-secret',
|
||||
BETTER_AUTH_URL: 'http://localhost:5173'
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('$lib/auth', () => ({
|
||||
auth: { api: authApi }
|
||||
}));
|
||||
|
||||
vi.mock('$lib/server/resend', () => ({
|
||||
sendMail: sendMailMock
|
||||
}));
|
||||
|
||||
vi.mock('$lib/db', () => ({
|
||||
prisma: {
|
||||
subscriptionLevel: { findFirst: vi.fn() },
|
||||
user: { findUnique: vi.fn() },
|
||||
verification: { findFirst: vi.fn() },
|
||||
licenseKey: { create: vi.fn() }
|
||||
}
|
||||
}));
|
||||
|
||||
import { prisma } from '$lib/db';
|
||||
import { POST } from './+server';
|
||||
|
||||
type POSTEvent = Parameters<typeof POST>[0];
|
||||
|
||||
const SECRET = 'test-onboarding-secret';
|
||||
const PATH = '/api/v2/automations/create/account';
|
||||
|
||||
const mockLevelFindFirst = prisma.subscriptionLevel.findFirst as ReturnType<typeof vi.fn>;
|
||||
const mockUserFindUnique = prisma.user.findUnique as ReturnType<typeof vi.fn>;
|
||||
const mockVerification = prisma.verification.findFirst as ReturnType<typeof vi.fn>;
|
||||
const mockLicenseCreate = prisma.licenseKey.create as ReturnType<typeof vi.fn>;
|
||||
|
||||
function signedOnboardingRequest(body: object, secret = SECRET): Request {
|
||||
const method = 'POST';
|
||||
const timestamp = String(Math.floor(Date.now() / 1000));
|
||||
const nonce = crypto.randomUUID();
|
||||
const rawBody = JSON.stringify(body);
|
||||
const signature = computeSignature(
|
||||
secret,
|
||||
buildCanonicalRequest(method, PATH, timestamp, nonce, rawBody)
|
||||
);
|
||||
|
||||
return new Request(`http://localhost${PATH}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'x-timestamp': timestamp,
|
||||
'x-nonce': nonce,
|
||||
'x-signature': signature
|
||||
},
|
||||
body: rawBody
|
||||
});
|
||||
}
|
||||
|
||||
async function expectHttpError(promise: unknown, status: number, message: string) {
|
||||
try {
|
||||
await promise;
|
||||
} catch (e) {
|
||||
expect(isHttpError(e)).toBe(true);
|
||||
const err = e as { status: number; body: { message: string } };
|
||||
expect(err.status).toBe(status);
|
||||
expect(err.body.message).toBe(message);
|
||||
return;
|
||||
}
|
||||
throw new Error('expected handler to throw an HttpError');
|
||||
}
|
||||
|
||||
describe('POST /api/v2/automations/create/account', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
_clearNonces();
|
||||
_clearRateLimits();
|
||||
});
|
||||
|
||||
it('creates a license key for an existing user without a new signup', async () => {
|
||||
mockLevelFindFirst.mockResolvedValue({ id: 1, name: 'Basic', description: 'General only' });
|
||||
mockUserFindUnique.mockResolvedValue({ id: 'user-1', email: 'user@example.com' });
|
||||
mockLicenseCreate.mockResolvedValue({ id: 'lic-1' });
|
||||
sendMailMock.mockResolvedValue({});
|
||||
|
||||
const res = await POST({
|
||||
request: signedOnboardingRequest({ email: 'user@example.com', subscription_level: 'Basic' }),
|
||||
url: new URL(`http://localhost${PATH}`)
|
||||
} as POSTEvent);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual({
|
||||
success: true,
|
||||
message: 'Account and license key created',
|
||||
user_id: 'user-1'
|
||||
});
|
||||
expect(authApi.signUpEmail).not.toHaveBeenCalled();
|
||||
expect(mockLicenseCreate).toHaveBeenCalledTimes(1);
|
||||
expect(sendMailMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('signs up a new user and sends registration + license emails', async () => {
|
||||
mockLevelFindFirst.mockResolvedValue({ id: 1, name: 'Basic', description: 'General only' });
|
||||
mockUserFindUnique
|
||||
.mockResolvedValueOnce(null)
|
||||
.mockResolvedValueOnce({ id: 'user-2', email: 'new@example.com' });
|
||||
mockVerification.mockResolvedValue({ value: 'verify-token' });
|
||||
mockLicenseCreate.mockResolvedValue({ id: 'lic-2' });
|
||||
sendMailMock.mockResolvedValue({});
|
||||
authApi.signUpEmail.mockResolvedValue({});
|
||||
|
||||
const res = await POST({
|
||||
request: signedOnboardingRequest({ email: 'new@example.com', subscription_level: 'Elite' }),
|
||||
url: new URL(`http://localhost${PATH}`)
|
||||
} as POSTEvent);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual({
|
||||
success: true,
|
||||
message: 'Account and license key created',
|
||||
user_id: 'user-2'
|
||||
});
|
||||
expect(authApi.signUpEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
body: expect.objectContaining({ email: 'new@example.com', name: 'new' })
|
||||
})
|
||||
);
|
||||
expect(sendMailMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('rejects a request without a valid signature', async () => {
|
||||
const req = new Request(`http://localhost${PATH}`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ email: 'user@example.com', subscription_level: 'Basic' })
|
||||
});
|
||||
|
||||
const res = await POST({ request: req, url: new URL(`http://localhost${PATH}`) } as POSTEvent);
|
||||
expect(res.status).toBe(401);
|
||||
expect(await res.json()).toEqual({ error: 'invalid signature' });
|
||||
});
|
||||
|
||||
it('rejects a request signed with the wrong secret', async () => {
|
||||
const res = await POST({
|
||||
request: signedOnboardingRequest(
|
||||
{ email: 'user@example.com', subscription_level: 'Basic' },
|
||||
'wrong-secret'
|
||||
),
|
||||
url: new URL(`http://localhost${PATH}`)
|
||||
} as POSTEvent);
|
||||
expect(res.status).toBe(401);
|
||||
expect(await res.json()).toEqual({ error: 'invalid signature' });
|
||||
});
|
||||
|
||||
it('rejects a missing email', async () => {
|
||||
await expectHttpError(
|
||||
POST({
|
||||
request: signedOnboardingRequest({ subscription_level: 'Basic' }),
|
||||
url: new URL(`http://localhost${PATH}`)
|
||||
} as POSTEvent),
|
||||
400,
|
||||
'email is required'
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects an oversized body with 413', async () => {
|
||||
const rawBody = JSON.stringify({
|
||||
email: 'user@example.com',
|
||||
subscription_level: 'Basic',
|
||||
name: 'x'.repeat(64 * 1024 + 1)
|
||||
});
|
||||
expect(rawBody.length).toBeGreaterThan(64 * 1024);
|
||||
|
||||
const req = new Request(`http://localhost${PATH}`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: rawBody
|
||||
});
|
||||
|
||||
await expectHttpError(
|
||||
POST({ request: req, url: new URL(`http://localhost${PATH}`) } as POSTEvent),
|
||||
413,
|
||||
'request body too large'
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects an unknown subscription level', async () => {
|
||||
mockLevelFindFirst.mockResolvedValue(null);
|
||||
await expectHttpError(
|
||||
POST({
|
||||
request: signedOnboardingRequest({
|
||||
email: 'user@example.com',
|
||||
subscription_level: 'Titanium'
|
||||
}),
|
||||
url: new URL(`http://localhost${PATH}`)
|
||||
} as POSTEvent),
|
||||
400,
|
||||
'subscription_level "Titanium" not found'
|
||||
);
|
||||
});
|
||||
|
||||
it('rate limits repeated requests for the same email', async () => {
|
||||
mockLevelFindFirst.mockResolvedValue({ id: 1, name: 'Basic', description: 'General only' });
|
||||
mockUserFindUnique.mockResolvedValue({ id: 'user-1', email: 'user@example.com' });
|
||||
mockLicenseCreate.mockResolvedValue({ id: 'lic-1' });
|
||||
sendMailMock.mockResolvedValue({});
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const res = await POST({
|
||||
request: signedOnboardingRequest({
|
||||
email: 'user@example.com',
|
||||
subscription_level: 'Basic'
|
||||
}),
|
||||
url: new URL(`http://localhost${PATH}`)
|
||||
} as POSTEvent);
|
||||
expect(res.status).toBe(200);
|
||||
}
|
||||
|
||||
const res = await POST({
|
||||
request: signedOnboardingRequest({ email: 'user@example.com', subscription_level: 'Basic' }),
|
||||
url: new URL(`http://localhost${PATH}`)
|
||||
} as POSTEvent);
|
||||
expect(res.status).toBe(429);
|
||||
expect(await res.json()).toEqual({ message: 'rate limit exceeded' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { RequestHandler } from './$types';
|
||||
import { prisma } from '$lib/db';
|
||||
import { jsonOk } from '$lib/server/v2-helpers';
|
||||
import { serializeCategory } from '$lib/server/v2-serializers';
|
||||
|
||||
export const GET: RequestHandler = async () => {
|
||||
const categories = await prisma.proclamationCategory.findMany();
|
||||
return jsonOk(categories.map(serializeCategory));
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { RequestHandler } from './$types';
|
||||
import { prisma } from '$lib/db';
|
||||
import { jsonOk } from '$lib/server/v2-helpers';
|
||||
import { CANONICAL_FREQUENCIES, CANONICAL_PALETTES } from '$lib/server/v2-serializers';
|
||||
|
||||
export const GET: RequestHandler = async () => {
|
||||
const rows = await prisma.colorPalette.findMany();
|
||||
const byId = new Map(rows.map((r) => [r.solfeggioFrequencyId, r.colors as unknown as string[]]));
|
||||
|
||||
const palettes = CANONICAL_FREQUENCIES.map((f) => ({
|
||||
solfeggioFrequencyId: f.id,
|
||||
colors: byId.get(f.id) ?? CANONICAL_PALETTES[f.id]
|
||||
}));
|
||||
|
||||
return jsonOk(palettes);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { RequestHandler } from './$types';
|
||||
import { jsonOk } from '$lib/server/v2-helpers';
|
||||
import { CANONICAL_FREQUENCIES } from '$lib/server/v2-serializers';
|
||||
|
||||
export const GET: RequestHandler = async () => {
|
||||
return jsonOk(CANONICAL_FREQUENCIES);
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { prisma } from '$lib/db';
|
||||
import { clientIp, jsonOk, rateLimited, readJsonBody } from '$lib/server/v2-helpers';
|
||||
import { rateLimit } from '$lib/server/v2-rate-limit';
|
||||
import { serializeLicense } from '$lib/server/v2-serializers';
|
||||
|
||||
const LICENSE_KEY_RE = /^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/;
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
const ip = clientIp(request);
|
||||
const rl = rateLimit({ key: `license:${ip}`, limit: 10, windowMs: 60_000 });
|
||||
if (!rl.ok) {
|
||||
return rateLimited(rl.retryAfterMs);
|
||||
}
|
||||
|
||||
const body = (await readJsonBody(request)) as Record<string, unknown>;
|
||||
const licenseKey = typeof body.license_key === 'string' ? body.license_key.trim() : '';
|
||||
const email =
|
||||
typeof body.email_address === 'string' ? body.email_address.trim().toLowerCase() : '';
|
||||
|
||||
if (!licenseKey) {
|
||||
throw error(400, 'license_key is required');
|
||||
}
|
||||
if (!email) {
|
||||
throw error(400, 'email_address is required');
|
||||
}
|
||||
if (email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
throw error(400, 'email_address is required');
|
||||
}
|
||||
if (!LICENSE_KEY_RE.test(licenseKey)) {
|
||||
throw error(400, 'license key not found');
|
||||
}
|
||||
|
||||
const storedKey = await prisma.licenseKey.findFirst({
|
||||
where: { key: licenseKey }
|
||||
});
|
||||
if (!storedKey) {
|
||||
throw error(400, 'license key not found');
|
||||
}
|
||||
if (!storedKey.active) {
|
||||
throw error(400, 'license key is not active');
|
||||
}
|
||||
|
||||
const userAccount = await prisma.user.findUnique({ where: { email } });
|
||||
if (!userAccount) {
|
||||
throw error(400, 'user does not exist');
|
||||
}
|
||||
if (storedKey.userId !== userAccount.id) {
|
||||
throw error(400, 'key does not belong to specified user');
|
||||
}
|
||||
|
||||
const level = await prisma.subscriptionLevel.findFirst({
|
||||
where: { name: storedKey.level }
|
||||
});
|
||||
|
||||
return jsonOk(
|
||||
serializeLicense({
|
||||
key: storedKey.key,
|
||||
emailAddress: email,
|
||||
levelName: storedKey.level,
|
||||
level
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,167 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { isHttpError } from '@sveltejs/kit';
|
||||
import { _clearRateLimits } from '$lib/server/v2-rate-limit';
|
||||
|
||||
vi.mock('$lib/db', () => ({
|
||||
prisma: {
|
||||
licenseKey: { findFirst: vi.fn() },
|
||||
user: { findUnique: vi.fn() },
|
||||
subscriptionLevel: { findFirst: vi.fn() }
|
||||
}
|
||||
}));
|
||||
|
||||
import { prisma } from '$lib/db';
|
||||
import { POST } from './+server';
|
||||
|
||||
type POSTEvent = Parameters<typeof POST>[0];
|
||||
|
||||
const mockKeyFindFirst = prisma.licenseKey.findFirst as ReturnType<typeof vi.fn>;
|
||||
const mockUserFindUnique = prisma.user.findUnique as ReturnType<typeof vi.fn>;
|
||||
const mockLevelFindFirst = prisma.subscriptionLevel.findFirst as ReturnType<typeof vi.fn>;
|
||||
|
||||
function licenseRequest(body: unknown): Request {
|
||||
return new Request('http://localhost/api/v2/license', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
}
|
||||
|
||||
async function expectHttpError(promise: unknown, status: number, message: string) {
|
||||
try {
|
||||
await promise;
|
||||
} catch (e) {
|
||||
expect(isHttpError(e)).toBe(true);
|
||||
const err = e as { status: number; body: { message: string } };
|
||||
expect(err.status).toBe(status);
|
||||
expect(err.body.message).toBe(message);
|
||||
return;
|
||||
}
|
||||
throw new Error('expected handler to throw an HttpError');
|
||||
}
|
||||
|
||||
const storedKey = {
|
||||
id: 'key-1',
|
||||
key: 'ABCD-EFGH-IJKL-MNOP',
|
||||
active: true,
|
||||
level: 'Advanced',
|
||||
userId: 'user-1'
|
||||
};
|
||||
|
||||
describe('POST /api/v2/license', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
_clearRateLimits();
|
||||
});
|
||||
|
||||
it('returns the subscription_level object for a valid key', async () => {
|
||||
mockKeyFindFirst.mockResolvedValue(storedKey);
|
||||
mockUserFindUnique.mockResolvedValue({ id: 'user-1', email: 'user@example.com' });
|
||||
mockLevelFindFirst.mockResolvedValue({
|
||||
id: 2,
|
||||
name: 'Advanced',
|
||||
description: 'General + Specific'
|
||||
});
|
||||
|
||||
const res = await POST({
|
||||
request: licenseRequest({ license_key: storedKey.key, email_address: 'user@example.com' })
|
||||
} as POSTEvent);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual({
|
||||
key: storedKey.key,
|
||||
email_address: 'user@example.com',
|
||||
subscription_level: { id: 2, name: 'Advanced', description: 'General + Specific' }
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to a structurally consistent subscription_level for an unknown level name', async () => {
|
||||
mockKeyFindFirst.mockResolvedValue({ ...storedKey, level: 'SomeCustomLevel' });
|
||||
mockUserFindUnique.mockResolvedValue({ id: 'user-1', email: 'user@example.com' });
|
||||
mockLevelFindFirst.mockResolvedValue(null);
|
||||
|
||||
const res = await POST({
|
||||
request: licenseRequest({ license_key: storedKey.key, email_address: 'user@example.com' })
|
||||
} as POSTEvent);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual({
|
||||
key: storedKey.key,
|
||||
email_address: 'user@example.com',
|
||||
subscription_level: { id: null, name: 'SomeCustomLevel', description: null }
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects a missing license_key', async () => {
|
||||
await expectHttpError(
|
||||
POST({ request: licenseRequest({ email_address: 'user@example.com' }) } as POSTEvent),
|
||||
400,
|
||||
'license_key is required'
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a missing email_address', async () => {
|
||||
await expectHttpError(
|
||||
POST({ request: licenseRequest({ license_key: storedKey.key }) } as POSTEvent),
|
||||
400,
|
||||
'email_address is required'
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects an unknown key', async () => {
|
||||
mockKeyFindFirst.mockResolvedValue(null);
|
||||
await expectHttpError(
|
||||
POST({
|
||||
request: licenseRequest({ license_key: storedKey.key, email_address: 'user@example.com' })
|
||||
} as POSTEvent),
|
||||
400,
|
||||
'license key not found'
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects an inactive key', async () => {
|
||||
mockKeyFindFirst.mockResolvedValue({ ...storedKey, active: false });
|
||||
await expectHttpError(
|
||||
POST({
|
||||
request: licenseRequest({ license_key: storedKey.key, email_address: 'user@example.com' })
|
||||
} as POSTEvent),
|
||||
400,
|
||||
'license key is not active'
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a nonexistent user', async () => {
|
||||
mockKeyFindFirst.mockResolvedValue(storedKey);
|
||||
mockUserFindUnique.mockResolvedValue(null);
|
||||
await expectHttpError(
|
||||
POST({
|
||||
request: licenseRequest({ license_key: storedKey.key, email_address: 'nobody@example.com' })
|
||||
} as POSTEvent),
|
||||
400,
|
||||
'user does not exist'
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a key owned by a different user', async () => {
|
||||
mockKeyFindFirst.mockResolvedValue(storedKey);
|
||||
mockUserFindUnique.mockResolvedValue({ id: 'user-2', email: 'other@example.com' });
|
||||
await expectHttpError(
|
||||
POST({
|
||||
request: licenseRequest({ license_key: storedKey.key, email_address: 'other@example.com' })
|
||||
} as POSTEvent),
|
||||
400,
|
||||
'key does not belong to specified user'
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a malformed license key format', async () => {
|
||||
await expectHttpError(
|
||||
POST({
|
||||
request: licenseRequest({ license_key: 'not-a-key', email_address: 'user@example.com' })
|
||||
} as POSTEvent),
|
||||
400,
|
||||
'license key not found'
|
||||
);
|
||||
expect(mockKeyFindFirst).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { prisma } from '$lib/db';
|
||||
import { jsonOk } from '$lib/server/v2-helpers';
|
||||
import {
|
||||
canonicalIdForFrequency,
|
||||
serializeProclamation,
|
||||
type ProclamationWithFrequency
|
||||
} from '$lib/server/v2-serializers';
|
||||
|
||||
const PROCLAMATION_INCLUDE = { solfeggioFrequency: true } as const;
|
||||
|
||||
export const GET: RequestHandler = async ({ request }) => {
|
||||
const licenseKey = request.headers.get('x-license-key') ?? '';
|
||||
const email = request.headers.get('x-email') ?? '';
|
||||
|
||||
const hasEntitlementHeaders = licenseKey !== '' || email !== '';
|
||||
|
||||
let proclamations: ProclamationWithFrequency[];
|
||||
|
||||
if (hasEntitlementHeaders) {
|
||||
if (licenseKey === '' || email === '') {
|
||||
throw error(400, 'not-authorized');
|
||||
}
|
||||
|
||||
const verifiedKey = await prisma.licenseKey.findUnique({
|
||||
where: { key: licenseKey }
|
||||
});
|
||||
if (!verifiedKey) {
|
||||
throw error(400, 'not-authorized');
|
||||
}
|
||||
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
include: { licenseKeys: true }
|
||||
});
|
||||
if (!existingUser) {
|
||||
throw error(400, 'not-authorized');
|
||||
}
|
||||
|
||||
const owned = existingUser.licenseKeys.some((k) => k.key === verifiedKey.key);
|
||||
if (!owned) {
|
||||
throw error(400, "this key doesn't appear to belong to you");
|
||||
}
|
||||
|
||||
const userProclamations = await prisma.userProclamations.findMany({
|
||||
where: { userId: existingUser.id },
|
||||
include: { proclamation: { include: PROCLAMATION_INCLUDE } }
|
||||
});
|
||||
|
||||
proclamations = userProclamations.map((up) => up.proclamation);
|
||||
} else {
|
||||
proclamations = await prisma.proclamation.findMany({
|
||||
include: PROCLAMATION_INCLUDE
|
||||
});
|
||||
}
|
||||
|
||||
const paletteRows = await prisma.colorPalette.findMany();
|
||||
const palettesByCanonicalId = new Map(
|
||||
paletteRows.map((p) => [p.solfeggioFrequencyId, p.colors as unknown as string[]])
|
||||
);
|
||||
|
||||
const records = proclamations
|
||||
.map((p) => {
|
||||
const canonicalId = canonicalIdForFrequency(p.solfeggioFrequency);
|
||||
const palette = canonicalId ? palettesByCanonicalId.get(canonicalId) : undefined;
|
||||
return serializeProclamation(p, palette);
|
||||
})
|
||||
.filter((r) => r !== null);
|
||||
|
||||
return jsonOk(records);
|
||||
};
|
||||
@@ -0,0 +1,177 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { isHttpError } from '@sveltejs/kit';
|
||||
|
||||
vi.mock('$lib/db', () => ({
|
||||
prisma: {
|
||||
proclamation: { findMany: vi.fn() },
|
||||
licenseKey: { findUnique: vi.fn() },
|
||||
user: { findUnique: vi.fn() },
|
||||
userProclamations: { findMany: vi.fn() },
|
||||
colorPalette: { findMany: vi.fn() }
|
||||
}
|
||||
}));
|
||||
|
||||
import { prisma } from '$lib/db';
|
||||
import { GET } from './+server';
|
||||
|
||||
type GETEvent = Parameters<typeof GET>[0];
|
||||
|
||||
const mockProclamationFindMany = prisma.proclamation.findMany as ReturnType<typeof vi.fn>;
|
||||
const mockLicenseKey = prisma.licenseKey.findUnique as ReturnType<typeof vi.fn>;
|
||||
const mockUser = prisma.user.findUnique as ReturnType<typeof vi.fn>;
|
||||
const mockUserProclamations = prisma.userProclamations.findMany as ReturnType<typeof vi.fn>;
|
||||
const mockColorPalette = prisma.colorPalette.findMany as ReturnType<typeof vi.fn>;
|
||||
|
||||
const palettes = [
|
||||
{
|
||||
solfeggioFrequencyId: 5,
|
||||
colors: ['#32CD32', '#3CB371', '#90EE90', '#98FB98', '#ADFFA9', '#C8FFD2', '#F0FFF0']
|
||||
},
|
||||
{
|
||||
solfeggioFrequencyId: 7,
|
||||
colors: ['#00BFFF', '#1E90FF', '#4169E1', '#6495ED', '#87CEEB', '#ADD8E6', '#F0F8FF']
|
||||
}
|
||||
];
|
||||
|
||||
const peaceProclamation = {
|
||||
id: 'b40fdcb9-703c-420e-af32-a081964c6f08',
|
||||
name: 'Peace & Relaxation',
|
||||
content: '<p>The text content</p>',
|
||||
categoryId: 5,
|
||||
subscriptionLevelId: 1,
|
||||
solfeggioFrequencyId: 5,
|
||||
solfeggioFrequency: { id: 5, name: 'Solar Plexus', value: 528 }
|
||||
};
|
||||
|
||||
const crown936 = {
|
||||
id: '936-hz-proclamation',
|
||||
name: 'Crown 936',
|
||||
content: '<p>no canonical id</p>',
|
||||
categoryId: 4,
|
||||
subscriptionLevelId: 1,
|
||||
solfeggioFrequencyId: 1,
|
||||
solfeggioFrequency: { id: 1, name: 'Crown', value: 936 }
|
||||
};
|
||||
|
||||
function getRequest(headers: Record<string, string> = {}): Request {
|
||||
return new Request('http://localhost/api/v2/proclamations', { headers });
|
||||
}
|
||||
|
||||
async function expectHttpError(promise: unknown, status: number, message: string) {
|
||||
try {
|
||||
await promise;
|
||||
} catch (e) {
|
||||
expect(isHttpError(e)).toBe(true);
|
||||
const err = e as { status: number; body: { message: string } };
|
||||
expect(err.status).toBe(status);
|
||||
expect(err.body.message).toBe(message);
|
||||
return;
|
||||
}
|
||||
throw new Error('expected handler to throw an HttpError');
|
||||
}
|
||||
|
||||
describe('GET /api/v2/proclamations', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns full proclamation records with canonical ids and embedded palettes', async () => {
|
||||
mockProclamationFindMany.mockResolvedValue([peaceProclamation]);
|
||||
mockColorPalette.mockResolvedValue(palettes);
|
||||
|
||||
const res = await GET({ request: getRequest() } as GETEvent);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual([
|
||||
{
|
||||
id: peaceProclamation.id,
|
||||
name: peaceProclamation.name,
|
||||
content: peaceProclamation.content,
|
||||
categoryId: 5,
|
||||
solfeggioFrequencyId: 5,
|
||||
subscriptionLevelId: 1,
|
||||
colorPalette: palettes[0].colors
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('drops proclamations whose frequency has no canonical id (936 Hz)', async () => {
|
||||
mockProclamationFindMany.mockResolvedValue([peaceProclamation, crown936]);
|
||||
mockColorPalette.mockResolvedValue(palettes);
|
||||
|
||||
const res = await GET({ request: getRequest() } as GETEvent);
|
||||
const body = (await res.json()) as { id: string }[];
|
||||
|
||||
expect(body).toHaveLength(1);
|
||||
expect(body[0].id).toBe(peaceProclamation.id);
|
||||
});
|
||||
|
||||
it('returns the entitled proclamations as full records when headers are supplied', async () => {
|
||||
mockLicenseKey.mockResolvedValue({ id: 'key-1', key: 'ABCD-EFGH-IJKL-MNOP' });
|
||||
mockUser.mockResolvedValue({
|
||||
id: 'user-1',
|
||||
email: 'user@example.com',
|
||||
licenseKeys: [{ id: 'key-1', key: 'ABCD-EFGH-IJKL-MNOP' }]
|
||||
});
|
||||
mockUserProclamations.mockResolvedValue([
|
||||
{ userId: 'user-1', proclamationId: peaceProclamation.id, proclamation: peaceProclamation }
|
||||
]);
|
||||
mockColorPalette.mockResolvedValue(palettes);
|
||||
|
||||
const res = await GET({
|
||||
request: getRequest({
|
||||
'x-license-key': 'ABCD-EFGH-IJKL-MNOP',
|
||||
'x-email': 'user@example.com'
|
||||
})
|
||||
} as GETEvent);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { id: string }[];
|
||||
expect(body).toHaveLength(1);
|
||||
expect(body[0].id).toBe(peaceProclamation.id);
|
||||
expect(mockUserProclamations).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ where: { userId: 'user-1' } })
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects when only one entitlement header is supplied', async () => {
|
||||
await expectHttpError(
|
||||
GET({ request: getRequest({ 'x-license-key': 'ABCD-EFGH-IJKL-MNOP' }) } as GETEvent),
|
||||
400,
|
||||
'not-authorized'
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects an unknown license key', async () => {
|
||||
mockLicenseKey.mockResolvedValue(null);
|
||||
await expectHttpError(
|
||||
GET({
|
||||
request: getRequest({
|
||||
'x-license-key': 'UNKNOWN-KEY',
|
||||
'x-email': 'user@example.com'
|
||||
})
|
||||
} as GETEvent),
|
||||
400,
|
||||
'not-authorized'
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a key that does not belong to the user', async () => {
|
||||
mockLicenseKey.mockResolvedValue({ id: 'key-2', key: 'OTHER-KEY' });
|
||||
mockUser.mockResolvedValue({
|
||||
id: 'user-1',
|
||||
email: 'user@example.com',
|
||||
licenseKeys: [{ id: 'key-1', key: 'ABCD-EFGH-IJKL-MNOP' }]
|
||||
});
|
||||
await expectHttpError(
|
||||
GET({
|
||||
request: getRequest({
|
||||
'x-license-key': 'OTHER-KEY',
|
||||
'x-email': 'user@example.com'
|
||||
})
|
||||
} as GETEvent),
|
||||
400,
|
||||
"this key doesn't appear to belong to you"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { RequestHandler } from './$types';
|
||||
import { prisma } from '$lib/db';
|
||||
import { jsonOk } from '$lib/server/v2-helpers';
|
||||
import { serializeSubscriptionLevel } from '$lib/server/v2-serializers';
|
||||
|
||||
export const GET: RequestHandler = async () => {
|
||||
const levels = await prisma.subscriptionLevel.findMany({
|
||||
orderBy: { id: 'asc' }
|
||||
});
|
||||
return jsonOk(levels.map(serializeSubscriptionLevel));
|
||||
};
|
||||
Reference in New Issue
Block a user