Add proclamations api endpoint
This commit is contained in:
@@ -83,23 +83,23 @@ No new route files, no new components. The registry is the only thing you touch.
|
||||
|
||||
### AdminModelConfig
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `slug` | `string` | Yes | URL segment (kebab-case). Used in `/admin/{slug}`. |
|
||||
| `modelName` | `string` | Yes | Prisma model name (PascalCase, **singular**). Maps to `prisma[lcfirst(modelName)]`. |
|
||||
| `displayName` | `string` | Yes | Plural display name for headings and navigation. |
|
||||
| `displayNameSingular` | `string` | Yes | Singular name for edit page headings. |
|
||||
| `idType` | `'String' \| 'Int'` | Yes | Primary key type. Controls how the `id` URL param is parsed. |
|
||||
| `include` | `Record<string, boolean \| { include: ... }>` | No | Prisma `include` clause for eager-loading relations. |
|
||||
| `listFields` | `string[]` | Yes | Table columns. Supports dot-notation for relations (e.g., `'role.name'`). |
|
||||
| `searchFields` | `string[]` | Yes | Fields searched by the client-side search filter. |
|
||||
| `formFields` | `string[]` | Yes | Fields rendered in create/edit forms, in display order. |
|
||||
| `readonlyFields` | `string[]` | No | Fields shown as read-only on the edit page. |
|
||||
| `listOnly` | `boolean` | No | If `true`, suppresses edit links in the table. |
|
||||
| `fieldTypes` | `Record<string, PrismaScalarType>` | Yes | Maps field names to scalar types. Controls input rendering and FormData parsing. |
|
||||
| `relationOptions` | `Record<string, RelationOptionConfig>` | No | FK fields that render as ComboBox dropdowns. |
|
||||
| `relatedRecords` | `RelatedRecordConfig[]` | No | Has-one/has-many panels shown on edit pages. |
|
||||
| `accessFilter` | `(viewerRoleId: number) => where \| undefined` | No | Returns a Prisma `where` clause to filter records by viewer role. `undefined` = no filter. |
|
||||
| Field | Type | Required | Description |
|
||||
| --------------------- | ---------------------------------------------- | -------- | ------------------------------------------------------------------------------------------ |
|
||||
| `slug` | `string` | Yes | URL segment (kebab-case). Used in `/admin/{slug}`. |
|
||||
| `modelName` | `string` | Yes | Prisma model name (PascalCase, **singular**). Maps to `prisma[lcfirst(modelName)]`. |
|
||||
| `displayName` | `string` | Yes | Plural display name for headings and navigation. |
|
||||
| `displayNameSingular` | `string` | Yes | Singular name for edit page headings. |
|
||||
| `idType` | `'String' \| 'Int'` | Yes | Primary key type. Controls how the `id` URL param is parsed. |
|
||||
| `include` | `Record<string, boolean \| { include: ... }>` | No | Prisma `include` clause for eager-loading relations. |
|
||||
| `listFields` | `string[]` | Yes | Table columns. Supports dot-notation for relations (e.g., `'role.name'`). |
|
||||
| `searchFields` | `string[]` | Yes | Fields searched by the client-side search filter. |
|
||||
| `formFields` | `string[]` | Yes | Fields rendered in create/edit forms, in display order. |
|
||||
| `readonlyFields` | `string[]` | No | Fields shown as read-only on the edit page. |
|
||||
| `listOnly` | `boolean` | No | If `true`, suppresses edit links in the table. |
|
||||
| `fieldTypes` | `Record<string, PrismaScalarType>` | Yes | Maps field names to scalar types. Controls input rendering and FormData parsing. |
|
||||
| `relationOptions` | `Record<string, RelationOptionConfig>` | No | FK fields that render as ComboBox dropdowns. |
|
||||
| `relatedRecords` | `RelatedRecordConfig[]` | No | Has-one/has-many panels shown on edit pages. |
|
||||
| `accessFilter` | `(viewerRoleId: number) => where \| undefined` | No | Returns a Prisma `where` clause to filter records by viewer role. `undefined` = no filter. |
|
||||
|
||||
### PrismaScalarType
|
||||
|
||||
@@ -107,13 +107,13 @@ No new route files, no new components. The registry is the only thing you touch.
|
||||
|
||||
Controls both form input rendering and FormData parsing:
|
||||
|
||||
| Type | HTML Input | Parse Logic |
|
||||
|---|---|---|
|
||||
| `String` | `<input type="text">` | `raw.toString()` |
|
||||
| `Int` | `<input type="number" step="1">` | `parseInt()`, empty → `null` |
|
||||
| `Float` | `<input type="number" step="0.01">` | `parseFloat()`, empty → `null` |
|
||||
| `Boolean` | `<input type="checkbox">` | `'on'` or `'true'` → `true` |
|
||||
| `DateTime` | `<input type="datetime-local">` | `new Date()`, empty → `null` |
|
||||
| Type | HTML Input | Parse Logic |
|
||||
| ---------- | ----------------------------------- | ------------------------------ |
|
||||
| `String` | `<input type="text">` | `raw.toString()` |
|
||||
| `Int` | `<input type="number" step="1">` | `parseInt()`, empty → `null` |
|
||||
| `Float` | `<input type="number" step="0.01">` | `parseFloat()`, empty → `null` |
|
||||
| `Boolean` | `<input type="checkbox">` | `'on'` or `'true'` → `true` |
|
||||
| `DateTime` | `<input type="datetime-local">` | `new Date()`, empty → `null` |
|
||||
|
||||
### RelationOptionConfig
|
||||
|
||||
@@ -184,20 +184,20 @@ Display has-one or has-many related records as panels on the edit page using `re
|
||||
|
||||
```ts
|
||||
relatedRecords: [
|
||||
{
|
||||
relationKey: 'profile',
|
||||
label: 'Profile',
|
||||
type: 'has-one',
|
||||
displayFields: ['firstName', 'lastName'],
|
||||
editSlug: 'profiles' // links to /admin/profiles/{id}
|
||||
},
|
||||
{
|
||||
relationKey: 'licenseKeys',
|
||||
label: 'License Keys',
|
||||
type: 'has-many',
|
||||
displayFields: ['key', 'active', 'level']
|
||||
}
|
||||
]
|
||||
{
|
||||
relationKey: 'profile',
|
||||
label: 'Profile',
|
||||
type: 'has-one',
|
||||
displayFields: ['firstName', 'lastName'],
|
||||
editSlug: 'profiles' // links to /admin/profiles/{id}
|
||||
},
|
||||
{
|
||||
relationKey: 'licenseKeys',
|
||||
label: 'License Keys',
|
||||
type: 'has-many',
|
||||
displayFields: ['key', 'active', 'level']
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
The `relationKey` must match a key in `include` so the data is actually loaded. `has-one` renders
|
||||
@@ -210,12 +210,12 @@ support an optional `editSlug` to link to the related record's edit page.
|
||||
|
||||
Defined in `roles.ts`. Lower numbers = higher privilege:
|
||||
|
||||
| Constant | Value | Access Level |
|
||||
|---|---|---|
|
||||
| `ROLES.SUPER` | 1 | Full access, sees all users |
|
||||
| `ROLES.ADMIN` | 2 | Admin access, sees staff and below |
|
||||
| `ROLES.STAFF` | 3 | Staff access (minimum for admin panel) |
|
||||
| `ROLES.USER` | 4 | Regular user (no admin access) |
|
||||
| Constant | Value | Access Level |
|
||||
| ------------- | ----- | -------------------------------------- |
|
||||
| `ROLES.SUPER` | 1 | Full access, sees all users |
|
||||
| `ROLES.ADMIN` | 2 | Admin access, sees staff and below |
|
||||
| `ROLES.STAFF` | 3 | Staff access (minimum for admin panel) |
|
||||
| `ROLES.USER` | 4 | Regular user (no admin access) |
|
||||
|
||||
### Layout Guard
|
||||
|
||||
@@ -228,12 +228,12 @@ Use `accessFilter` to restrict which records a viewer can see:
|
||||
|
||||
```ts
|
||||
accessFilter: (viewerRoleId: number) => {
|
||||
if (viewerRoleId <= ROLES.SUPER) return undefined; // no filter
|
||||
if (viewerRoleId <= ROLES.ADMIN) {
|
||||
return { roleId: { gte: ROLES.STAFF } }; // can't see other admins
|
||||
}
|
||||
return { roleId: { gt: ROLES.STAFF } }; // staff sees only regular users
|
||||
}
|
||||
if (viewerRoleId <= ROLES.SUPER) return undefined; // no filter
|
||||
if (viewerRoleId <= ROLES.ADMIN) {
|
||||
return { roleId: { gte: ROLES.STAFF } }; // can't see other admins
|
||||
}
|
||||
return { roleId: { gt: ROLES.STAFF } }; // staff sees only regular users
|
||||
};
|
||||
```
|
||||
|
||||
## How Dynamic Routing Works
|
||||
@@ -250,74 +250,74 @@ accessFilter: (viewerRoleId: number) => {
|
||||
|
||||
### AdminTable
|
||||
|
||||
| Prop | Type | Description |
|
||||
|---|---|---|
|
||||
| `records` | `Record<string, unknown>[]` | Data rows |
|
||||
| `listFields` | `string[]` | Column keys (dot-notation supported) |
|
||||
| `searchFields` | `string[]` | Fields for client-side search |
|
||||
| `modelSlug` | `string` | Used for building edit links |
|
||||
| `listOnly` | `boolean?` | Suppress edit links |
|
||||
| Prop | Type | Description |
|
||||
| -------------- | --------------------------- | ------------------------------------ |
|
||||
| `records` | `Record<string, unknown>[]` | Data rows |
|
||||
| `listFields` | `string[]` | Column keys (dot-notation supported) |
|
||||
| `searchFields` | `string[]` | Fields for client-side search |
|
||||
| `modelSlug` | `string` | Used for building edit links |
|
||||
| `listOnly` | `boolean?` | Suppress edit links |
|
||||
|
||||
Features: client-side search, click-to-sort columns, auto-formatted cells (dates, booleans,
|
||||
decimals), filtered count display.
|
||||
|
||||
### AdminForm
|
||||
|
||||
| Prop | Type | Description |
|
||||
|---|---|---|
|
||||
| `fields` | `string[]` | Field names to render |
|
||||
| `fieldTypes` | `Record<string, PrismaScalarType>` | Type mapping for inputs |
|
||||
| `relationOptions` | `Record<string, RelationOptionConfig>?` | FK dropdown configs |
|
||||
| `relationOptionsData` | `Record<string, { label; value }[]>?` | Loaded dropdown options |
|
||||
| `record` | `Record<string, unknown>?` | Pre-fill values (edit mode) |
|
||||
| `form` | `ActionData?` | Carries validation errors |
|
||||
| `action` | `string` | Form action (e.g., `'?/create'`) |
|
||||
| `submitLabel` | `string?` | Button text |
|
||||
| Prop | Type | Description |
|
||||
| --------------------- | --------------------------------------- | -------------------------------- |
|
||||
| `fields` | `string[]` | Field names to render |
|
||||
| `fieldTypes` | `Record<string, PrismaScalarType>` | Type mapping for inputs |
|
||||
| `relationOptions` | `Record<string, RelationOptionConfig>?` | FK dropdown configs |
|
||||
| `relationOptionsData` | `Record<string, { label; value }[]>?` | Loaded dropdown options |
|
||||
| `record` | `Record<string, unknown>?` | Pre-fill values (edit mode) |
|
||||
| `form` | `ActionData?` | Carries validation errors |
|
||||
| `action` | `string` | Form action (e.g., `'?/create'`) |
|
||||
| `submitLabel` | `string?` | Button text |
|
||||
|
||||
### ComboBox
|
||||
|
||||
| Prop | Type | Description |
|
||||
|---|---|---|
|
||||
| `id` | `string` | Element ID |
|
||||
| `name` | `string` | Form field name |
|
||||
| `options` | `{ label; value }[]` | Dropdown items |
|
||||
| `placeholder` | `string?` | Placeholder text |
|
||||
| `value` | `string \| number?` | Pre-selected value |
|
||||
| Prop | Type | Description |
|
||||
| ------------- | -------------------- | ------------------ |
|
||||
| `id` | `string` | Element ID |
|
||||
| `name` | `string` | Form field name |
|
||||
| `options` | `{ label; value }[]` | Dropdown items |
|
||||
| `placeholder` | `string?` | Placeholder text |
|
||||
| `value` | `string \| number?` | Pre-selected value |
|
||||
|
||||
Features: type-to-search filtering, keyboard navigation (ArrowUp/Down, Enter, Escape),
|
||||
clear button, scroll-to-highlight.
|
||||
|
||||
### RelatedHasOne
|
||||
|
||||
| Prop | Type | Description |
|
||||
|---|---|---|
|
||||
| `label` | `string` | Section heading |
|
||||
| `record` | `Record<string, unknown> \| null` | Related record |
|
||||
| `displayFields` | `string[]` | Fields to display as key-value pairs |
|
||||
| `editSlug` | `string?` | Links to edit the related record |
|
||||
| Prop | Type | Description |
|
||||
| --------------- | --------------------------------- | ------------------------------------ |
|
||||
| `label` | `string` | Section heading |
|
||||
| `record` | `Record<string, unknown> \| null` | Related record |
|
||||
| `displayFields` | `string[]` | Fields to display as key-value pairs |
|
||||
| `editSlug` | `string?` | Links to edit the related record |
|
||||
|
||||
### RelatedHasMany
|
||||
|
||||
| Prop | Type | Description |
|
||||
|---|---|---|
|
||||
| `label` | `string` | Section heading (shows count) |
|
||||
| `records` | `Record<string, unknown>[]` | Related records |
|
||||
| `displayFields` | `string[]` | Table columns |
|
||||
| `editSlug` | `string?` | Per-row edit links |
|
||||
| Prop | Type | Description |
|
||||
| --------------- | --------------------------- | ----------------------------- |
|
||||
| `label` | `string` | Section heading (shows count) |
|
||||
| `records` | `Record<string, unknown>[]` | Related records |
|
||||
| `displayFields` | `string[]` | Table columns |
|
||||
| `editSlug` | `string?` | Per-row edit links |
|
||||
|
||||
## Utility Functions
|
||||
|
||||
| Function | Module | Description |
|
||||
|---|---|---|
|
||||
| `fieldNameToLabel(name)` | `introspect.ts` | `'solfeggioFrequencyId'` → `'Solfeggio Frequency'` (strips `Id`, adds spaces) |
|
||||
| `resolveFieldValue(record, path)` | `introspect.ts` | Resolves dot-notation paths like `'category.name'` on nested objects |
|
||||
| `extractFormData(formData, fields, types)` | `form-data.ts` | Parses `FormData` with type-aware conversion per `PrismaScalarType` |
|
||||
| `getDelegate(config)` | `prisma-delegate.ts` | Returns `prisma[lcfirst(modelName)]` delegate |
|
||||
| `parseId(config, raw)` | `prisma-delegate.ts` | Parses string to `number` when `idType` is `'Int'` |
|
||||
| `getModelConfig(slug)` | `registry.ts` | Looks up config by URL slug |
|
||||
| `getAllModelConfigs()` | `registry.ts` | Returns all registered model configs |
|
||||
| `hasRole(roleId, required)` | `roles.ts` | `true` if `roleId <= required` |
|
||||
| `canAccessAdmin(roleId)` | `roles.ts` | `true` if `roleId <= ROLES.STAFF` |
|
||||
| Function | Module | Description |
|
||||
| ------------------------------------------ | -------------------- | ----------------------------------------------------------------------------- |
|
||||
| `fieldNameToLabel(name)` | `introspect.ts` | `'solfeggioFrequencyId'` → `'Solfeggio Frequency'` (strips `Id`, adds spaces) |
|
||||
| `resolveFieldValue(record, path)` | `introspect.ts` | Resolves dot-notation paths like `'category.name'` on nested objects |
|
||||
| `extractFormData(formData, fields, types)` | `form-data.ts` | Parses `FormData` with type-aware conversion per `PrismaScalarType` |
|
||||
| `getDelegate(config)` | `prisma-delegate.ts` | Returns `prisma[lcfirst(modelName)]` delegate |
|
||||
| `parseId(config, raw)` | `prisma-delegate.ts` | Parses string to `number` when `idType` is `'Int'` |
|
||||
| `getModelConfig(slug)` | `registry.ts` | Looks up config by URL slug |
|
||||
| `getAllModelConfigs()` | `registry.ts` | Returns all registered model configs |
|
||||
| `hasRole(roleId, required)` | `roles.ts` | `true` if `roleId <= required` |
|
||||
| `canAccessAdmin(roleId)` | `roles.ts` | `true` if `roleId <= ROLES.STAFF` |
|
||||
|
||||
## Complete Example
|
||||
|
||||
|
||||
Reference in New Issue
Block a user