@meith/settings 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +165 -0
- package/package.json +26 -0
- package/src/board-url.ts +26 -0
- package/src/branding.ts +21 -0
- package/src/definitions.ts +1088 -0
- package/src/fields.ts +58 -0
- package/src/index.ts +69 -0
- package/src/legal.ts +117 -0
- package/src/mail.ts +281 -0
- package/src/origin.ts +52 -0
- package/src/push.ts +44 -0
- package/src/store.ts +135 -0
package/src/store.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { ValidationError } from '@meith/core'
|
|
2
|
+
|
|
3
|
+
import type { SettingDefinition, SettingKey, SettingValue } from './definitions'
|
|
4
|
+
import { SETTING_DEFINITION_BY_KEY, SETTING_DEFINITIONS } from './definitions'
|
|
5
|
+
|
|
6
|
+
export interface SettingsRepository {
|
|
7
|
+
loadAll(): Promise<ReadonlyMap<string, string>>
|
|
8
|
+
save(entries: ReadonlyMap<string, string>): Promise<void>
|
|
9
|
+
delete(keys: readonly string[]): Promise<void>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SettingsSnapshotOptions {
|
|
13
|
+
onInvalid?: (key: string, raw: string, reason: string) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function serialise(value: unknown): string {
|
|
17
|
+
if (typeof value === 'boolean') return value ? '1' : '0'
|
|
18
|
+
if (typeof value === 'number') return String(value)
|
|
19
|
+
if (typeof value === 'string') return value
|
|
20
|
+
return JSON.stringify(value)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function coerce(definition: SettingDefinition<unknown>, raw: string): unknown {
|
|
24
|
+
const expected = definition.default
|
|
25
|
+
|
|
26
|
+
let candidate: unknown = raw
|
|
27
|
+
if (typeof expected === 'boolean') {
|
|
28
|
+
candidate = raw === '1' || raw === 'true'
|
|
29
|
+
} else if (typeof expected === 'number') {
|
|
30
|
+
candidate = Number(raw)
|
|
31
|
+
} else if (typeof expected === 'object' && expected !== null) {
|
|
32
|
+
try {
|
|
33
|
+
candidate = JSON.parse(raw)
|
|
34
|
+
} catch {
|
|
35
|
+
candidate = raw
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return definition.schema.parse(candidate)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class SettingsSnapshot {
|
|
43
|
+
private constructor(private readonly values: ReadonlyMap<string, unknown>) {}
|
|
44
|
+
|
|
45
|
+
static fromOverrides(
|
|
46
|
+
overrides: ReadonlyMap<string, string>,
|
|
47
|
+
options: SettingsSnapshotOptions = {},
|
|
48
|
+
): SettingsSnapshot {
|
|
49
|
+
const resolved = new Map<string, unknown>()
|
|
50
|
+
|
|
51
|
+
for (const definition of SETTING_DEFINITIONS) {
|
|
52
|
+
const raw = overrides.get(definition.key)
|
|
53
|
+
|
|
54
|
+
if (raw === undefined) {
|
|
55
|
+
resolved.set(definition.key, definition.default)
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
resolved.set(definition.key, coerce(definition as SettingDefinition<unknown>, raw))
|
|
61
|
+
} catch (error) {
|
|
62
|
+
options.onInvalid?.(
|
|
63
|
+
definition.key,
|
|
64
|
+
raw,
|
|
65
|
+
error instanceof Error ? error.message : String(error),
|
|
66
|
+
)
|
|
67
|
+
resolved.set(definition.key, definition.default)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return new SettingsSnapshot(resolved)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get<K extends SettingKey>(key: K): SettingValue<K> {
|
|
75
|
+
return this.values.get(key) as SettingValue<K>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
toObject(): Record<string, unknown> {
|
|
79
|
+
return Object.fromEntries(this.values)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface SaveResult {
|
|
84
|
+
changed: string[]
|
|
85
|
+
invalidates: string[]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function saveSettings(
|
|
89
|
+
repository: SettingsRepository,
|
|
90
|
+
updates: Readonly<Record<string, unknown>>,
|
|
91
|
+
current: SettingsSnapshot,
|
|
92
|
+
): Promise<SaveResult> {
|
|
93
|
+
const toWrite = new Map<string, string>()
|
|
94
|
+
const toDelete: string[] = []
|
|
95
|
+
const changed: string[] = []
|
|
96
|
+
const invalidates = new Set<string>()
|
|
97
|
+
const errors: string[] = []
|
|
98
|
+
|
|
99
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
100
|
+
const definition = SETTING_DEFINITION_BY_KEY.get(key)
|
|
101
|
+
|
|
102
|
+
if (!definition) {
|
|
103
|
+
errors.push(`Unknown setting "${key}".`)
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const parsed = definition.schema.safeParse(value)
|
|
108
|
+
if (!parsed.success) {
|
|
109
|
+
errors.push(`${key}: ${parsed.error.issues.map((i) => i.message).join('; ')}`)
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (Object.is(parsed.data, current.get(key as SettingKey))) {
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
changed.push(key)
|
|
118
|
+
for (const tag of definition.invalidates ?? []) invalidates.add(tag)
|
|
119
|
+
|
|
120
|
+
if (Object.is(parsed.data, definition.default)) {
|
|
121
|
+
toDelete.push(key)
|
|
122
|
+
} else {
|
|
123
|
+
toWrite.set(key, serialise(parsed.data))
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (errors.length > 0) {
|
|
128
|
+
throw new ValidationError(`Invalid settings: ${errors.join(' ')}`)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (toWrite.size > 0) await repository.save(toWrite)
|
|
132
|
+
if (toDelete.length > 0) await repository.delete(toDelete)
|
|
133
|
+
|
|
134
|
+
return { changed, invalidates: [...invalidates] }
|
|
135
|
+
}
|