db: drizzle schema matching spec 02, initial migrations, settings service
Migration 0000 enables citext; 0001 creates all 39 tables. DB tests use a throwaway database per file (TEST_DATABASE_URL, derived from .env in dev, a postgres service in CI). Decision 0002: pg driver and @fastify/cookie. Task: M1-T1
This commit is contained in:
1 parent
63a03b4185
commit
15b8a20751
29 files changed
+6649
-25
No files matched your search
@@ -17,5 +17,8 @@
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
],
|
||||
"dependencies": {
|
||||
"zod": "^4.6.7"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// "Where a call goes next" (spec 02 §1). Stored as jsonb in many tables.
|
||||
import { z } from 'zod';
|
||||
|
||||
export const DestinationSchema = z.discriminatedUnion('type', [
|
||||
z.object({ type: z.literal('user'), id: z.uuid() }), // extensions.id; rings, then that user's no-answer handling
|
||||
z.object({ type: z.literal('voicemail'), id: z.uuid() }), // extensions.id; straight to voicemail
|
||||
z.object({ type: z.literal('ring_group'), id: z.uuid() }),
|
||||
z.object({ type: z.literal('queue'), id: z.uuid() }),
|
||||
z.object({ type: z.literal('ivr'), id: z.uuid() }),
|
||||
z.object({ type: z.literal('time_condition'), id: z.uuid() }),
|
||||
z.object({ type: z.literal('conference'), id: z.uuid() }),
|
||||
z.object({ type: z.literal('announcement'), id: z.uuid() }),
|
||||
z.object({ type: z.literal('external'), number: z.string().regex(/^[0-9*#+]+$/) }), // normalized external number
|
||||
z.object({ type: z.literal('hangup') }),
|
||||
]);
|
||||
|
||||
export type Destination = z.infer<typeof DestinationSchema>;
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
export const APP_NAME = 'SelfPhoneNG';
|
||||
export * from './phone.ts';
|
||||
export * from './destination.ts';
|
||||
export * from './settings.ts';
|
||||
@@ -0,0 +1,84 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// One zod schema (with its default) per settings key (spec 02 §3).
|
||||
import { z } from 'zod';
|
||||
import { DestinationSchema } from './destination.ts';
|
||||
|
||||
const range = (from: number, to: number) =>
|
||||
z
|
||||
.tuple([z.number().int().min(100), z.number().int().max(999999)])
|
||||
.refine(([a, b]) => a <= b, 'range start must be <= end')
|
||||
.default([from, to]);
|
||||
|
||||
export const SettingsSchemas = {
|
||||
'general.companyName': z.string().trim().min(1).max(100).default('My Company'),
|
||||
// The api replaces this default with env TZ (services/settings.ts).
|
||||
'general.timezone': z.string().min(1).default('America/Chicago'),
|
||||
'general.language': z.string().min(2).max(10).default('en'),
|
||||
'network.publicIp': z.ipv4().nullable().default(null),
|
||||
'network.localNets': z
|
||||
.array(z.cidrv4())
|
||||
.default(['192.168.0.0/16', '10.0.0.0/8', '172.16.0.0/12']),
|
||||
'network.lanIp': z.ipv4().nullable().default(null),
|
||||
'network.publicIpWatch': z.boolean().default(false),
|
||||
'network.stunServer': z
|
||||
.string()
|
||||
.regex(/^[A-Za-z0-9.-]+(:\d{1,5})?$/)
|
||||
.nullable()
|
||||
.default(null),
|
||||
'network.turn': z
|
||||
.object({ url: z.string().min(1), username: z.string(), credentialEnc: z.string() })
|
||||
.nullable()
|
||||
.default(null),
|
||||
'sip.udpPort': z.number().int().min(1).max(65535).default(5060),
|
||||
'sip.tcpEnabled': z.boolean().default(true),
|
||||
'sip.tlsEnabled': z.boolean().default(false),
|
||||
'sip.tlsPort': z.number().int().min(1).max(65535).default(5061),
|
||||
'numbering.users': range(100, 499),
|
||||
'numbering.ringGroups': range(500, 549),
|
||||
'numbering.queues': range(550, 599),
|
||||
'numbering.ivrs': range(600, 649),
|
||||
'numbering.conferences': range(800, 849),
|
||||
'numbering.paging': range(850, 869),
|
||||
'routing.unmatchedDidDestination': DestinationSchema.default({ type: 'hangup' }),
|
||||
'routing.defaultCallerIdDidId': z.uuid().nullable().default(null),
|
||||
smtp: z
|
||||
.object({
|
||||
host: z.string().min(1),
|
||||
port: z.number().int().min(1).max(65535),
|
||||
secure: z.boolean(),
|
||||
username: z.string(),
|
||||
passwordEnc: z.string(),
|
||||
from: z.string().min(3),
|
||||
})
|
||||
.nullable()
|
||||
.default(null),
|
||||
'voicemail.maxMessageSeconds': z.number().int().min(10).max(3600).default(300),
|
||||
'voicemail.maxMessages': z.number().int().min(1).max(9999).default(100),
|
||||
'voicemail.retentionDays': z.number().int().min(0).default(0),
|
||||
'recordings.retentionDays': z.number().int().min(0).default(90),
|
||||
'recordings.announcementSoundId': z.uuid().nullable().default(null),
|
||||
'emergency.defaultCallerIdDidId': z.uuid().nullable().default(null),
|
||||
'emergency.notifyEmails': z.array(z.email()).default([]),
|
||||
'emergency.notifyExtensionIds': z.array(z.uuid()).default([]),
|
||||
'backup.enabled': z.boolean().default(true),
|
||||
'backup.time': z
|
||||
.string()
|
||||
.regex(/^([01]\d|2[0-3]):[0-5]\d$/)
|
||||
.default('03:30'),
|
||||
'backup.keep': z.number().int().min(1).max(365).default(7),
|
||||
'backup.includeRecordings': z.boolean().default(false),
|
||||
'webhooks.allowPrivate': z.boolean().default(false),
|
||||
'setup.completed': z.boolean().default(false),
|
||||
// The api generates and stores a random value on first read (services/settings.ts).
|
||||
'sms.webhookToken': z.string().regex(/^[A-Za-z0-9]{32}$/),
|
||||
'sip.restrictPhonesToLocalNets': z.boolean().default(false),
|
||||
'sip.extraAllowedNets': z.array(z.cidrv4()).default([]),
|
||||
'provisioning.lanOnly': z.boolean().default(false),
|
||||
'provisioning.enabled': z.boolean().default(true),
|
||||
'system.restartRequired': z.boolean().default(false),
|
||||
// Not in 02 §3's table but used by the generator (03 §3).
|
||||
'system.transportHash': z.string().nullable().default(null),
|
||||
};
|
||||
|
||||
export type SettingKey = keyof typeof SettingsSchemas;
|
||||
export type SettingValue<K extends SettingKey> = z.output<(typeof SettingsSchemas)[K]>;
|
||||
Reference in new issue
Block a user