- Add per-route form components: UserRoleForm, UserForm, ProclamationForm (with category cascade), CategoryForm, FreqForm, LevelForm - Update all admin +page.svelte files to use AdminTable + AdminModal + their specific form component instead of AdminPage - Update profiles page to inline fields instead of using AdminForm - Add custom Proclamation server action with category fallback logic (categoryId falls back to parentCategoryId when no sub-category) - Remove cascade/were/fieldLabels logic from generic crud.ts - Remove outdated cascade/where tests from crud.spec.ts - Remove fieldLabels from AdminModelConfig type
191 lines
5.7 KiB
TypeScript
191 lines
5.7 KiB
TypeScript
import { getDelegate, parseId } from './prisma-delegate';
|
|
import { extractFormData } from './form-data';
|
|
import type { AdminModelConfig } from './types';
|
|
import { superValidate } from 'sveltekit-superforms/server';
|
|
import { zod4 } from 'sveltekit-superforms/adapters';
|
|
import * as Schemas from './schemas';
|
|
import type { z } from 'zod';
|
|
|
|
const modelSchemas: Record<string, z.ZodObject<any>> = {
|
|
User: Schemas.User,
|
|
Profile: Schemas.Profile,
|
|
UserRole: Schemas.UserRole,
|
|
Proclamation: Schemas.Proclamation,
|
|
ProclamationCategory: Schemas.ProclamationCategory,
|
|
SolfeggioFrequency: Schemas.SolfeggioFrequency,
|
|
SubscriptionLevel: Schemas.SubscriptionLevel,
|
|
LicenseKey: Schemas.LicenseKey
|
|
};
|
|
|
|
export function superFormErrorsToFieldErrors(superErrors: Record<string, unknown>): {
|
|
error: string;
|
|
fieldErrors: Record<string, string>;
|
|
} {
|
|
const fieldErrors: Record<string, string> = {};
|
|
let formError = '';
|
|
for (const [key, value] of Object.entries(superErrors)) {
|
|
if (key === '_errors' && Array.isArray(value)) {
|
|
formError = (value[0] as string) ?? '';
|
|
} else if (
|
|
value &&
|
|
typeof value === 'object' &&
|
|
'_errors' in value &&
|
|
Array.isArray((value as Record<string, unknown>)._errors)
|
|
) {
|
|
const errMsg = ((value as Record<string, unknown>)._errors as string[])[0];
|
|
fieldErrors[key] = errMsg;
|
|
}
|
|
}
|
|
return { error: formError, fieldErrors };
|
|
}
|
|
|
|
export async function loadRecords(locals: App.Locals, config: AdminModelConfig) {
|
|
const delegate = getDelegate(config);
|
|
|
|
const viewerRoleId = (locals.user as { roleId?: number } | null)?.roleId ?? 4;
|
|
const where = config.accessFilter?.(viewerRoleId);
|
|
|
|
const records = await delegate.findMany({
|
|
where,
|
|
include: config.include,
|
|
orderBy: { id: 'asc' }
|
|
});
|
|
|
|
const relationOptionsData: Record<string, { label: string; value: string | number }[]> = {};
|
|
|
|
for (const [field, relConfig] of Object.entries(config.relationOptions ?? {})) {
|
|
const relDelegate = getDelegate({ ...config, modelName: relConfig.model });
|
|
const selectFields: Record<string, boolean> = {
|
|
[relConfig.valueField]: true,
|
|
[relConfig.labelField]: true
|
|
};
|
|
const options = await relDelegate.findMany({
|
|
select: selectFields,
|
|
orderBy: { [relConfig.labelField]: 'asc' }
|
|
});
|
|
const mapped = (options as Record<string, unknown>[]).map((opt: Record<string, unknown>) => ({
|
|
label: String(opt[relConfig.labelField]),
|
|
value: opt[relConfig.valueField] as string | number
|
|
}));
|
|
relationOptionsData[field] = mapped;
|
|
}
|
|
|
|
return {
|
|
records: records as Record<string, unknown>[],
|
|
relationOptionsData
|
|
};
|
|
}
|
|
|
|
export async function upsertRecord(
|
|
requestOrFormData: Request | FormData,
|
|
config: AdminModelConfig,
|
|
locals: App.Locals
|
|
) {
|
|
const formData =
|
|
requestOrFormData instanceof FormData ? requestOrFormData : await requestOrFormData.formData();
|
|
|
|
const schema = modelSchemas[config.modelName];
|
|
const form = schema ? await superValidate(formData, zod4(schema)) : null;
|
|
|
|
if (form && !form.valid) {
|
|
const { error, fieldErrors } = superFormErrorsToFieldErrors(
|
|
form.errors as Record<string, unknown>
|
|
);
|
|
return { ...form, success: false, error, fieldErrors };
|
|
}
|
|
|
|
const rawId = formData.get('id')?.toString();
|
|
const data =
|
|
form?.data ??
|
|
(extractFormData(formData, config.formFields, config.fieldTypes) as Record<string, unknown>);
|
|
|
|
// Strip virtual/UI-only fields from Prisma payload
|
|
for (const f of config.dataOnlyFields ?? []) {
|
|
delete data[f];
|
|
}
|
|
|
|
const delegate = getDelegate(config);
|
|
const viewerRoleId = (locals.user as { roleId?: number } | null)?.roleId ?? 4;
|
|
const where = config.accessFilter?.(viewerRoleId);
|
|
|
|
if (rawId) {
|
|
const id = parseId(config, rawId);
|
|
|
|
const existing = await delegate.findUnique({
|
|
where: { id },
|
|
...((config.include ? { include: config.include } : {}) as object)
|
|
});
|
|
if (!existing) {
|
|
return { success: false, error: 'Record not found' };
|
|
}
|
|
|
|
if (where) {
|
|
const match = await delegate.findFirst({
|
|
where: { id, ...where }
|
|
});
|
|
if (!match) {
|
|
return { success: false, error: 'Access denied' };
|
|
}
|
|
}
|
|
|
|
await delegate.update({
|
|
where: { id },
|
|
data
|
|
});
|
|
|
|
return { success: true };
|
|
} else {
|
|
try {
|
|
await delegate.create({ data });
|
|
} catch (e: unknown) {
|
|
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2002') {
|
|
const meta = (e as { meta?: { target?: string[]; modelName?: string } }).meta;
|
|
const fields = meta?.target ?? [];
|
|
console.error(
|
|
`[crud] P2002 on ${meta?.modelName ?? 'unknown'} create. data=`,
|
|
data,
|
|
`meta.target=`,
|
|
meta?.target
|
|
);
|
|
if (fields.length === 0) {
|
|
return {
|
|
success: false,
|
|
error: 'A database constraint violation occurred. The record could not be created.'
|
|
};
|
|
}
|
|
return {
|
|
success: false,
|
|
error: `A record with this ${fields.join(', ')} already exists.`,
|
|
fieldErrors: Object.fromEntries(fields.map((f) => [f, 'Already taken']))
|
|
};
|
|
}
|
|
throw e;
|
|
}
|
|
return { success: true };
|
|
}
|
|
}
|
|
|
|
export async function deleteRecord(
|
|
requestOrFormData: Request | FormData,
|
|
config: AdminModelConfig,
|
|
locals: App.Locals
|
|
) {
|
|
const delegate = getDelegate(config);
|
|
const formData =
|
|
requestOrFormData instanceof FormData ? requestOrFormData : await requestOrFormData.formData();
|
|
const rawId = formData.get('id')?.toString();
|
|
if (!rawId) return { success: false, error: 'Missing id' };
|
|
|
|
const id = parseId(config, rawId);
|
|
|
|
const viewerRoleId = (locals.user as { roleId?: number } | null)?.roleId ?? 4;
|
|
const where = config.accessFilter?.(viewerRoleId);
|
|
if (where) {
|
|
const match = await delegate.findFirst({ where: { id, ...where } });
|
|
if (!match) return { success: false, error: 'Access denied' };
|
|
}
|
|
|
|
await delegate.delete({ where: { id } });
|
|
return { success: true };
|
|
}
|