348 lines
12 KiB
TypeScript
348 lines
12 KiB
TypeScript
/**
|
|
* CMS Content Type Registry
|
|
*
|
|
* Single source of truth for all managed content types.
|
|
* Follows the WordPress post-type registration pattern.
|
|
*/
|
|
|
|
export type FieldType =
|
|
| 'text' // TEXT column — serialized as string | null
|
|
| 'number' // INTEGER or REAL column — serialized as number | null
|
|
| 'boolean' // INTEGER 0|1 column — serialized as boolean
|
|
| 'enum' // TEXT with allowed values — validated on write
|
|
| 'json-array' // TEXT column storing JSON.stringify(string[]) — serialized as string[]
|
|
| 'json-object'; // TEXT column storing JSON.stringify(obj) — serialized as Record<string,unknown>
|
|
|
|
export interface FieldDef {
|
|
type: FieldType;
|
|
nullable?: boolean; // if true, null is a valid value
|
|
enum?: readonly string[]; // required when type === 'enum'
|
|
default?: unknown; // used when field is absent from PUT body
|
|
}
|
|
|
|
export type ContentKind = 'singleton' | 'list' | 'hierarchical' | 'kv-store';
|
|
|
|
export interface ContentTypeDef {
|
|
kind: ContentKind;
|
|
table: string;
|
|
path: string; // URL path segment (e.g. 'identity', 'tour', 'rates')
|
|
sortable?: boolean; // list/hierarchical: has sort_order column
|
|
childTable?: string; // hierarchical: child table name
|
|
childForeignKey?: string; // hierarchical: FK column on child table
|
|
kvNamespaceCol?: string; // kv-store: namespace column name
|
|
kvKeyCol?: string; // kv-store: key column name
|
|
fields: Record<string, FieldDef>; // DB column name → field def (excludes id, sort_order, updated_at)
|
|
childFields?: Record<string, FieldDef>; // hierarchical child fields
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Registry
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const registry: Record<string, ContentTypeDef> = {
|
|
identity: {
|
|
kind: 'singleton',
|
|
table: 'identity',
|
|
path: 'identity',
|
|
fields: {
|
|
name: { type: 'text' },
|
|
pronouns: { type: 'text' },
|
|
gender: { type: 'text' },
|
|
location: { type: 'text' },
|
|
incall_city: { type: 'text', nullable: true },
|
|
tagline: { type: 'text' },
|
|
secondary_locations: { type: 'json-array', default: [] },
|
|
languages: { type: 'json-array', default: [] },
|
|
},
|
|
},
|
|
|
|
physical: {
|
|
kind: 'singleton',
|
|
table: 'physical',
|
|
path: 'physical',
|
|
fields: {
|
|
age: { type: 'text', nullable: true },
|
|
height: { type: 'text', nullable: true },
|
|
body_type: { type: 'text', nullable: true },
|
|
ethnicity: { type: 'text', nullable: true },
|
|
hair_color: { type: 'text', nullable: true },
|
|
hair_length:{ type: 'text', nullable: true },
|
|
eye_color: { type: 'text', nullable: true },
|
|
cup_size: { type: 'text', nullable: true },
|
|
additional: { type: 'json-object', default: {} },
|
|
},
|
|
},
|
|
|
|
contact: {
|
|
kind: 'singleton',
|
|
table: 'contact',
|
|
path: 'contact',
|
|
fields: {
|
|
phone: { type: 'text' },
|
|
whatsapp: { type: 'text', nullable: true },
|
|
email: { type: 'text', nullable: true },
|
|
instagram: { type: 'text', nullable: true },
|
|
twitter: { type: 'text', nullable: true },
|
|
threads: { type: 'text', nullable: true },
|
|
snapchat: { type: 'text', nullable: true },
|
|
youtube: { type: 'text', nullable: true },
|
|
onlyfans: { type: 'text', nullable: true },
|
|
transfans: { type: 'text', nullable: true },
|
|
fansly: { type: 'text', nullable: true },
|
|
loyalfans: { type: 'text', nullable: true },
|
|
fancentro: { type: 'text', nullable: true },
|
|
fantime: { type: 'text', nullable: true },
|
|
tryst: { type: 'text', nullable: true },
|
|
communication_note: { type: 'text' },
|
|
response_time: { type: 'text' },
|
|
availability_note: { type: 'text', nullable: true },
|
|
payment_methods: { type: 'json-array', default: [] },
|
|
},
|
|
},
|
|
|
|
about: {
|
|
kind: 'singleton',
|
|
table: 'about',
|
|
path: 'about',
|
|
fields: {
|
|
bio: { type: 'text' },
|
|
personality: { type: 'json-array', default: [] },
|
|
available_for: { type: 'json-array', default: [] },
|
|
available_to: { type: 'json-array', default: [] },
|
|
},
|
|
},
|
|
|
|
tour: {
|
|
kind: 'list',
|
|
table: 'tour_stops',
|
|
path: 'tour',
|
|
sortable: true,
|
|
fields: {
|
|
city: { type: 'text' },
|
|
state: { type: 'text' },
|
|
start_date: { type: 'text' },
|
|
end_date: { type: 'text' },
|
|
status: { type: 'enum', enum: ['confirmed', 'conditional', 'sold-out'] as const },
|
|
notes: { type: 'text', nullable: true },
|
|
},
|
|
},
|
|
|
|
destinations: {
|
|
kind: 'list',
|
|
table: 'destinations',
|
|
path: 'destinations',
|
|
sortable: true,
|
|
fields: {
|
|
slug: { type: 'text' },
|
|
city: { type: 'text' },
|
|
country: { type: 'text' },
|
|
region: { type: 'text', nullable: true },
|
|
fmty_tier: { type: 'enum', enum: ['west-coast', 'domestic', 'international'] as const },
|
|
meta_title: { type: 'text' },
|
|
meta_description: { type: 'text' },
|
|
headline: { type: 'text' },
|
|
intro: { type: 'text' },
|
|
linked_tour_stop: { type: 'boolean' },
|
|
experiences: { type: 'json-array', default: [] },
|
|
note: { type: 'text', nullable: true },
|
|
illustration_image: { type: 'text', nullable: true },
|
|
illustration_side: { type: 'enum', enum: ['left', 'right'] as const, default: 'right' },
|
|
illustration_height: { type: 'text', default: '360px' },
|
|
illustration_opacity: { type: 'number', default: 0.85 },
|
|
relationship: { type: 'enum', enum: ['homebase', 'metro-neighbor', 'tour-confirmed', 'tour-aspirational'] as const, default: 'tour-aspirational' },
|
|
super_region: { type: 'text', nullable: true },
|
|
neighborhoods: { type: 'json-array', default: [] },
|
|
local_incall_notes: { type: 'text', nullable: true },
|
|
driving_time_mins: { type: 'number', nullable: true },
|
|
affluence_tier: { type: 'enum', enum: ['premier', 'high', 'mid'] as const, default: 'high' },
|
|
},
|
|
},
|
|
|
|
specialties: {
|
|
kind: 'list',
|
|
table: 'specialties',
|
|
path: 'specialties',
|
|
sortable: true,
|
|
fields: {
|
|
category_slug: { type: 'text' },
|
|
category_name: { type: 'text' },
|
|
category_meta_title: { type: 'text' },
|
|
category_meta_description: { type: 'text' },
|
|
category_intro: { type: 'text' },
|
|
slug: { type: 'text' },
|
|
name: { type: 'text' },
|
|
meta_title: { type: 'text' },
|
|
meta_description: { type: 'text' },
|
|
headline: { type: 'text' },
|
|
intro: { type: 'text' },
|
|
includes: { type: 'json-array', nullable: true },
|
|
note: { type: 'text', nullable: true },
|
|
related_rate_type: { type: 'text', nullable: true },
|
|
illustration_image: { type: 'text', nullable: true },
|
|
illustration_side: { type: 'enum', enum: ['left', 'right'] as const, default: 'right' },
|
|
illustration_height: { type: 'text', default: '360px' },
|
|
illustration_opacity: { type: 'number', default: 0.85 },
|
|
},
|
|
},
|
|
|
|
'activity-menus': {
|
|
kind: 'list',
|
|
table: 'activity_menus',
|
|
path: 'activity-menus',
|
|
sortable: true,
|
|
fields: {
|
|
category: { type: 'text' },
|
|
items: { type: 'json-array', default: [] },
|
|
},
|
|
},
|
|
|
|
rates: {
|
|
kind: 'hierarchical',
|
|
table: 'rate_sections',
|
|
path: 'rates',
|
|
sortable: true,
|
|
childTable: 'rate_entries',
|
|
childForeignKey: 'section_id',
|
|
fields: {
|
|
section_type: { type: 'enum', enum: ['incall', 'outcall', 'addons', 'travel', 'touring', 'online'] as const },
|
|
title: { type: 'text' },
|
|
description: { type: 'text', nullable: true },
|
|
},
|
|
childFields: {
|
|
service: { type: 'text' },
|
|
duration: { type: 'text', nullable: true },
|
|
price: { type: 'number' },
|
|
price_max: { type: 'number', nullable: true },
|
|
description: { type: 'text', nullable: true },
|
|
notes: { type: 'text', nullable: true },
|
|
},
|
|
},
|
|
|
|
policies: {
|
|
kind: 'hierarchical',
|
|
table: 'policy_sections',
|
|
path: 'policies',
|
|
sortable: true,
|
|
childTable: 'policy_items',
|
|
childForeignKey: 'section_id',
|
|
fields: {
|
|
title: { type: 'text' },
|
|
},
|
|
childFields: {
|
|
label: { type: 'text' },
|
|
detail: { type: 'text' },
|
|
},
|
|
},
|
|
|
|
etiquette: {
|
|
kind: 'hierarchical',
|
|
table: 'etiquette_sections',
|
|
path: 'etiquette',
|
|
sortable: true,
|
|
childTable: 'etiquette_items',
|
|
childForeignKey: 'section_id',
|
|
fields: {
|
|
title: { type: 'text' },
|
|
},
|
|
childFields: {
|
|
label: { type: 'text' },
|
|
detail: { type: 'text', nullable: true },
|
|
cta_href: { type: 'text', nullable: true },
|
|
cta_text: { type: 'text', nullable: true },
|
|
},
|
|
},
|
|
|
|
'verified-profiles': {
|
|
kind: 'list',
|
|
table: 'verified_profiles',
|
|
path: 'verified-profiles',
|
|
sortable: true,
|
|
fields: {
|
|
platform: { type: 'text' },
|
|
href: { type: 'text' },
|
|
img_src: { type: 'text' },
|
|
img_alt: { type: 'text' },
|
|
embed_html: { type: 'text' },
|
|
description: { type: 'text' },
|
|
},
|
|
},
|
|
|
|
'site-text': {
|
|
kind: 'kv-store',
|
|
table: 'site_text',
|
|
path: 'site-text',
|
|
kvNamespaceCol: 'namespace',
|
|
kvKeyCol: 'key',
|
|
fields: {
|
|
value: { type: 'text' },
|
|
},
|
|
},
|
|
|
|
'link-values': {
|
|
kind: 'kv-store',
|
|
table: 'link_values',
|
|
path: 'link-values',
|
|
kvNamespaceCol: 'event_name',
|
|
kvKeyCol: 'label',
|
|
fields: {
|
|
score: { type: 'number' },
|
|
},
|
|
},
|
|
|
|
'hobby-terms': {
|
|
kind: 'list',
|
|
table: 'hobby_terms',
|
|
path: 'hobby-terms',
|
|
sortable: true,
|
|
fields: {
|
|
slug: { type: 'text' },
|
|
term: { type: 'text' },
|
|
aliases: { type: 'json-array', default: [] },
|
|
definition_md: { type: 'text' },
|
|
definition_html: { type: 'text' },
|
|
category: { type: 'enum', enum: ['service', 'logistics', 'kink', 'community', 'general', 'identity'] as const, default: 'general' },
|
|
offered: { type: 'enum', enum: ['yes', 'no', 'adjacent'] as const, default: 'no' },
|
|
safety_notes: { type: 'text', default: '' },
|
|
related_terms: { type: 'json-array', default: [] },
|
|
meta_title: { type: 'text', default: '' },
|
|
meta_description: { type: 'text', default: '' },
|
|
},
|
|
},
|
|
|
|
regions: {
|
|
kind: 'list',
|
|
table: 'regions',
|
|
path: 'regions',
|
|
sortable: true,
|
|
fields: {
|
|
slug: { type: 'text' },
|
|
display_name: { type: 'text' },
|
|
super_region: { type: 'text', nullable: true },
|
|
tier: { type: 'enum', enum: ['super-region', 'region', 'sub-region'] as const, default: 'region' },
|
|
intro_md: { type: 'text', default: '' },
|
|
intro_html: { type: 'text', default: '' },
|
|
meta_title: { type: 'text', default: '' },
|
|
meta_description: { type: 'text', default: '' },
|
|
sibling_regions: { type: 'json-array', default: [] },
|
|
},
|
|
},
|
|
|
|
'positioning-tags': {
|
|
kind: 'list',
|
|
table: 'positioning_tags',
|
|
path: 'positioning-tags',
|
|
sortable: true,
|
|
fields: {
|
|
slug: { type: 'text' },
|
|
display: { type: 'text' },
|
|
description_md: { type: 'text', default: '' },
|
|
description_html: { type: 'text', default: '' },
|
|
target_markets: { type: 'json-array', default: [] },
|
|
related_terms: { type: 'json-array', default: [] },
|
|
similar_to: { type: 'json-array', default: [] },
|
|
active: { type: 'boolean', default: true },
|
|
meta_title: { type: 'text', default: '' },
|
|
meta_description: { type: 'text', default: '' },
|
|
},
|
|
},
|
|
};
|