@southwind-ai/shared 0.1.1 → 0.2.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/index.ts +2 -0
- package/package.json +1 -1
- package/src/audit.ts +25 -0
- package/src/json.ts +8 -0
- package/src/license-catalog.ts +95 -0
- package/src/migration-bundle-validation.ts +219 -0
- package/src/module-configuration.ts +5 -0
- package/src/notification.ts +92 -1
- package/src/password.ts +7 -2
- package/src/platform-settings.ts +31 -0
package/index.ts
CHANGED
|
@@ -4,7 +4,9 @@ export * from "./src/data-governance.js";
|
|
|
4
4
|
export * from "./src/feature.js";
|
|
5
5
|
export * from "./src/license.js";
|
|
6
6
|
export * from "./src/license-catalog.js";
|
|
7
|
+
export * from "./src/json.js";
|
|
7
8
|
export * from "./src/migration.js";
|
|
9
|
+
export * from "./src/migration-bundle-validation.js";
|
|
8
10
|
export * from "./src/module-configuration.js";
|
|
9
11
|
export * from "./src/correlation.js";
|
|
10
12
|
export * from "./src/notification.js";
|
package/package.json
CHANGED
package/src/audit.ts
CHANGED
|
@@ -145,11 +145,36 @@ export const AUDIT_ACTION_DEFINITIONS = [
|
|
|
145
145
|
label: "通知发布",
|
|
146
146
|
description: "发布站内通知。",
|
|
147
147
|
},
|
|
148
|
+
{
|
|
149
|
+
key: "notification.schedule",
|
|
150
|
+
label: "通知定时发布",
|
|
151
|
+
description: "创建等待调度器发布的通知。",
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
key: "notification.preference.update",
|
|
155
|
+
label: "通知偏好更新",
|
|
156
|
+
description: "用户更新外部通知渠道和分类偏好。",
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
key: "notification.update",
|
|
160
|
+
label: "通知修改",
|
|
161
|
+
description: "修改已发布通知并保留版本历史。",
|
|
162
|
+
},
|
|
148
163
|
{
|
|
149
164
|
key: "notification.read",
|
|
150
165
|
label: "通知已读",
|
|
151
166
|
description: "用户阅读站内通知。",
|
|
152
167
|
},
|
|
168
|
+
{
|
|
169
|
+
key: "notification.favorite",
|
|
170
|
+
label: "收藏通知",
|
|
171
|
+
description: "用户收藏可见的站内通知。",
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
key: "notification.unfavorite",
|
|
175
|
+
label: "取消收藏通知",
|
|
176
|
+
description: "用户取消收藏可见的站内通知。",
|
|
177
|
+
},
|
|
153
178
|
{
|
|
154
179
|
key: "notification.archive",
|
|
155
180
|
label: "通知归档",
|
package/src/json.ts
ADDED
package/src/license-catalog.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface LicenseCatalogDefinition {
|
|
|
7
7
|
order: number;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
const licenseCatalogFields = new Set(["key", "label", "description", "order"]);
|
|
11
|
+
|
|
10
12
|
export const LICENSE_CATALOG = [
|
|
11
13
|
{
|
|
12
14
|
key: licenseVersionKey("free-trial"),
|
|
@@ -31,3 +33,96 @@ export const LICENSE_CATALOG = [
|
|
|
31
33
|
export function licenseCatalogKeys(): ReadonlySet<LicenseVersionKey> {
|
|
32
34
|
return new Set(LICENSE_CATALOG.map(({ key }) => key));
|
|
33
35
|
}
|
|
36
|
+
|
|
37
|
+
/** 校验宿主配置的 License 版本目录:Key 与 order 均唯一,目录非空。 */
|
|
38
|
+
export function validateLicenseCatalog(
|
|
39
|
+
definitions: readonly LicenseCatalogDefinition[],
|
|
40
|
+
): void {
|
|
41
|
+
if (definitions.length === 0) {
|
|
42
|
+
throw new Error("License 版本目录不能为空");
|
|
43
|
+
}
|
|
44
|
+
const keys = new Set<string>();
|
|
45
|
+
const orders = new Set<number>();
|
|
46
|
+
for (const definition of definitions) {
|
|
47
|
+
if (!definition.key.trim() || !definition.label.trim()) {
|
|
48
|
+
throw new Error("License 版本目录的 Key 与名称不能为空");
|
|
49
|
+
}
|
|
50
|
+
if (!Number.isSafeInteger(definition.order)) {
|
|
51
|
+
throw new Error(`License 版本 ${definition.key} 的 order 必须是安全整数`);
|
|
52
|
+
}
|
|
53
|
+
if (keys.has(definition.key)) {
|
|
54
|
+
throw new Error(`License 版本目录 Key 重复:${definition.key}`);
|
|
55
|
+
}
|
|
56
|
+
if (orders.has(definition.order)) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`License 版本目录 order 重复:${definition.order}(${definition.key})`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
keys.add(definition.key);
|
|
62
|
+
orders.add(definition.order);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 从未知输入解析并归一化宿主 License 版本目录。 */
|
|
67
|
+
export function parseLicenseCatalog(
|
|
68
|
+
input: unknown,
|
|
69
|
+
): LicenseCatalogDefinition[] {
|
|
70
|
+
if (!Array.isArray(input)) {
|
|
71
|
+
throw new Error("License 版本目录必须是数组");
|
|
72
|
+
}
|
|
73
|
+
const catalog = input.map((entry, index) => {
|
|
74
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
75
|
+
throw new Error(`License 版本目录第 ${index + 1} 项必须是对象`);
|
|
76
|
+
}
|
|
77
|
+
const record = entry as Record<string, unknown>;
|
|
78
|
+
const unknownField = Object.keys(record).find(
|
|
79
|
+
(key) => !licenseCatalogFields.has(key),
|
|
80
|
+
);
|
|
81
|
+
if (unknownField) {
|
|
82
|
+
throw new Error(`License 版本目录包含未知字段:${unknownField}`);
|
|
83
|
+
}
|
|
84
|
+
if (
|
|
85
|
+
typeof record.key !== "string" ||
|
|
86
|
+
typeof record.label !== "string" ||
|
|
87
|
+
(record.description !== undefined &&
|
|
88
|
+
typeof record.description !== "string") ||
|
|
89
|
+
typeof record.order !== "number"
|
|
90
|
+
) {
|
|
91
|
+
throw new Error(`License 版本目录第 ${index + 1} 项字段类型无效`);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
key: licenseVersionKey(record.key.trim()),
|
|
95
|
+
label: record.label.trim(),
|
|
96
|
+
description: record.description?.trim() ?? "",
|
|
97
|
+
order: record.order,
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
validateLicenseCatalog(catalog);
|
|
101
|
+
return catalog;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 解析环境变量 JSON,错误消息不包含原始配置内容。 */
|
|
105
|
+
export function parseLicenseCatalogJson(
|
|
106
|
+
raw: string,
|
|
107
|
+
source = "License 版本目录",
|
|
108
|
+
): LicenseCatalogDefinition[] {
|
|
109
|
+
let parsed: unknown;
|
|
110
|
+
try {
|
|
111
|
+
parsed = JSON.parse(raw);
|
|
112
|
+
} catch {
|
|
113
|
+
throw new Error(`${source} 必须是合法 JSON 数组`);
|
|
114
|
+
}
|
|
115
|
+
if (!Array.isArray(parsed)) {
|
|
116
|
+
throw new Error(`${source} 必须是合法 JSON 数组`);
|
|
117
|
+
}
|
|
118
|
+
return parseLicenseCatalog(parsed);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 有宿主配置时校验并返回,未配置时回退平台默认三级目录。 */
|
|
122
|
+
export function resolveLicenseCatalog(
|
|
123
|
+
configured?: readonly LicenseCatalogDefinition[],
|
|
124
|
+
): readonly LicenseCatalogDefinition[] {
|
|
125
|
+
if (!configured) return LICENSE_CATALOG;
|
|
126
|
+
validateLicenseCatalog(configured);
|
|
127
|
+
return configured;
|
|
128
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MIGRATION_BUNDLE_FORMAT_VERSION,
|
|
3
|
+
type BundleMigrationDefinition,
|
|
4
|
+
type MigrationBundle,
|
|
5
|
+
type MigrationDefinition,
|
|
6
|
+
type MigrationManifestSource,
|
|
7
|
+
} from "./migration.js";
|
|
8
|
+
|
|
9
|
+
export function normalizeManifestBundles(
|
|
10
|
+
modules: readonly MigrationManifestSource[],
|
|
11
|
+
): MigrationBundle[] {
|
|
12
|
+
return modules.map((manifest) => {
|
|
13
|
+
const bundle =
|
|
14
|
+
manifest.migrationBundle ||
|
|
15
|
+
legacyManifestBundle(manifest, manifest.migrations);
|
|
16
|
+
if (bundle.module !== manifest.name) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Migration Bundle ${bundle.module} 与 Manifest ${manifest.name} 不一致`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
if (bundle.version !== manifest.version) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Migration Bundle ${bundle.module} 版本 ${bundle.version} 与 Manifest ${manifest.version} 不一致`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
assertSameDependencies(manifest, bundle);
|
|
27
|
+
assertSameMigrations(manifest, bundle);
|
|
28
|
+
assertSchemaMigrationReferences(manifest, bundle);
|
|
29
|
+
return bundle;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function assertSameDependencies(
|
|
34
|
+
manifest: MigrationManifestSource,
|
|
35
|
+
bundle: MigrationBundle,
|
|
36
|
+
): void {
|
|
37
|
+
const declared = manifest.dependencies || [];
|
|
38
|
+
const bundled = bundle.dependencies || [];
|
|
39
|
+
if (
|
|
40
|
+
declared.length !== bundled.length ||
|
|
41
|
+
declared.some((dependency) => !bundled.includes(dependency))
|
|
42
|
+
) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Manifest ${manifest.name} 的 dependencies 必须与 Migration Bundle 一致`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function legacyManifestBundle(
|
|
50
|
+
manifest: MigrationManifestSource,
|
|
51
|
+
migrations: readonly MigrationDefinition[],
|
|
52
|
+
): MigrationBundle {
|
|
53
|
+
return {
|
|
54
|
+
formatVersion: MIGRATION_BUNDLE_FORMAT_VERSION,
|
|
55
|
+
module: manifest.name,
|
|
56
|
+
version: manifest.version,
|
|
57
|
+
dependencies: manifest.dependencies,
|
|
58
|
+
migrations: migrations.map((migration) => ({
|
|
59
|
+
...migration,
|
|
60
|
+
checksum:
|
|
61
|
+
migration.checksum ||
|
|
62
|
+
`legacy:${manifest.name}:${manifest.version}:${migration.id}`,
|
|
63
|
+
})),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function assertSameMigrations(
|
|
68
|
+
manifest: MigrationManifestSource,
|
|
69
|
+
bundle: MigrationBundle,
|
|
70
|
+
): void {
|
|
71
|
+
const declared = manifest.migrations.map(({ id }) => id);
|
|
72
|
+
const bundled = bundle.migrations.map(({ id }) => id);
|
|
73
|
+
if (
|
|
74
|
+
declared.length !== bundled.length ||
|
|
75
|
+
declared.some((id, index) => id !== bundled[index])
|
|
76
|
+
) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Manifest ${manifest.name} 的 migrations 必须与 Migration Bundle 完全一致`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function assertSchemaMigrationReferences(
|
|
84
|
+
manifest: MigrationManifestSource,
|
|
85
|
+
bundle: MigrationBundle,
|
|
86
|
+
): void {
|
|
87
|
+
const ids = new Set(bundle.migrations.map(({ id }) => id));
|
|
88
|
+
for (const schema of manifest.schemaExports || []) {
|
|
89
|
+
for (const migrationId of schema.migrationIds) {
|
|
90
|
+
if (!ids.has(migrationId)) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Schema Export ${schema.key} 引用了 Bundle 中不存在的 Migration:${migrationId}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function validateAndOrderBundles(
|
|
100
|
+
bundles: readonly MigrationBundle[],
|
|
101
|
+
): MigrationBundle[] {
|
|
102
|
+
const byModule = new Map<string, MigrationBundle>();
|
|
103
|
+
const migrationOwner = new Map<string, string>();
|
|
104
|
+
for (const bundle of bundles) {
|
|
105
|
+
validateBundle(bundle);
|
|
106
|
+
if (byModule.has(bundle.module)) {
|
|
107
|
+
throw new Error(`Migration Bundle 模块重复:${bundle.module}`);
|
|
108
|
+
}
|
|
109
|
+
byModule.set(bundle.module, bundle);
|
|
110
|
+
for (const migration of bundle.migrations) {
|
|
111
|
+
const owner = migrationOwner.get(migration.id);
|
|
112
|
+
if (owner) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Migration ID 重复:${migration.id}(${owner} / ${bundle.module})`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
migrationOwner.set(migration.id, bundle.module);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const bundle of bundles) {
|
|
121
|
+
for (const dependency of bundle.dependencies || []) {
|
|
122
|
+
if (!byModule.has(dependency)) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Migration Bundle ${bundle.module} 依赖未安装模块:${dependency}`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
validateMigrationDependencies(bundle, migrationOwner);
|
|
129
|
+
orderBundleMigrations(bundle);
|
|
130
|
+
}
|
|
131
|
+
return topologicalBundles(byModule);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function validateBundle(bundle: MigrationBundle): void {
|
|
135
|
+
if (bundle.formatVersion !== MIGRATION_BUNDLE_FORMAT_VERSION) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Migration Bundle ${bundle.module} 格式版本不受支持:${bundle.formatVersion}`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (!bundle.module.trim()) throw new Error("Migration Bundle 缺少 module");
|
|
141
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(bundle.version)) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`Migration Bundle ${bundle.module} 版本无效:${bundle.version}`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
for (const migration of bundle.migrations) {
|
|
147
|
+
if (migration.module !== bundle.module) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`Migration ${migration.id} 的模块归属应为 ${bundle.module}`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
if (!migration.id.trim()) throw new Error("Migration ID 不能为空");
|
|
153
|
+
if (!migration.checksum.trim()) {
|
|
154
|
+
throw new Error(`Migration ${migration.id} 缺少 checksum`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function validateMigrationDependencies(
|
|
160
|
+
bundle: MigrationBundle,
|
|
161
|
+
owners: ReadonlyMap<string, string>,
|
|
162
|
+
): void {
|
|
163
|
+
for (const migration of bundle.migrations) {
|
|
164
|
+
for (const dependency of migration.dependsOn || []) {
|
|
165
|
+
const owner = owners.get(dependency);
|
|
166
|
+
if (!owner) {
|
|
167
|
+
throw new Error(`Migration ${migration.id} 依赖不存在:${dependency}`);
|
|
168
|
+
}
|
|
169
|
+
if (
|
|
170
|
+
owner !== bundle.module &&
|
|
171
|
+
!(bundle.dependencies || []).includes(owner)
|
|
172
|
+
) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`Migration ${migration.id} 跨模块依赖 ${owner},但 Bundle 未声明该模块依赖`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function topologicalBundles(
|
|
182
|
+
pendingSource: ReadonlyMap<string, MigrationBundle>,
|
|
183
|
+
): MigrationBundle[] {
|
|
184
|
+
const pending = new Map(pendingSource);
|
|
185
|
+
const sorted: MigrationBundle[] = [];
|
|
186
|
+
while (pending.size > 0) {
|
|
187
|
+
const ready = Array.from(pending.values()).find((bundle) =>
|
|
188
|
+
(bundle.dependencies || []).every(
|
|
189
|
+
(dependency) => !pending.has(dependency),
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
if (!ready) throw new Error("Migration Bundle 模块依赖存在循环");
|
|
193
|
+
sorted.push(ready);
|
|
194
|
+
pending.delete(ready.module);
|
|
195
|
+
}
|
|
196
|
+
return sorted;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function orderBundleMigrations(
|
|
200
|
+
bundle: MigrationBundle,
|
|
201
|
+
): BundleMigrationDefinition[] {
|
|
202
|
+
const pending = new Map(
|
|
203
|
+
bundle.migrations.map((migration) => [migration.id, migration]),
|
|
204
|
+
);
|
|
205
|
+
const sorted: BundleMigrationDefinition[] = [];
|
|
206
|
+
while (pending.size > 0) {
|
|
207
|
+
const ready = Array.from(pending.values()).find((migration) =>
|
|
208
|
+
(migration.dependsOn || []).every(
|
|
209
|
+
(dependency) => !pending.has(dependency),
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
if (!ready) {
|
|
213
|
+
throw new Error(`Migration Bundle ${bundle.module} 的迁移依赖存在循环`);
|
|
214
|
+
}
|
|
215
|
+
sorted.push(ready);
|
|
216
|
+
pending.delete(ready.id);
|
|
217
|
+
}
|
|
218
|
+
return sorted;
|
|
219
|
+
}
|
|
@@ -5,8 +5,11 @@ export type ModuleConfigApplyStatus =
|
|
|
5
5
|
| "applying"
|
|
6
6
|
| "effective"
|
|
7
7
|
| "failed"
|
|
8
|
+
| "pending-restart"
|
|
8
9
|
| "superseded";
|
|
9
10
|
|
|
11
|
+
export type ModuleConfigApplyMode = "live" | "restart";
|
|
12
|
+
|
|
10
13
|
export interface ModuleConfigDiffEntry {
|
|
11
14
|
path: string;
|
|
12
15
|
before?: ModuleConfigValue;
|
|
@@ -18,6 +21,7 @@ export interface ModuleConfigFieldDescriptor {
|
|
|
18
21
|
label: string;
|
|
19
22
|
description: string;
|
|
20
23
|
input: "boolean" | "integer" | "text";
|
|
24
|
+
applyMode?: ModuleConfigApplyMode;
|
|
21
25
|
minimum?: number;
|
|
22
26
|
maximum?: number;
|
|
23
27
|
}
|
|
@@ -42,6 +46,7 @@ export interface ModuleConfigSummary {
|
|
|
42
46
|
label: string;
|
|
43
47
|
fields: ModuleConfigFieldDescriptor[];
|
|
44
48
|
effectiveValues: ModuleConfigValues;
|
|
49
|
+
pendingValues?: ModuleConfigValues;
|
|
45
50
|
effectiveSource: "environment" | "version";
|
|
46
51
|
effectiveVersion?: number;
|
|
47
52
|
latestVersion?: number;
|
package/src/notification.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import type { ISODateTime } from "./primitives.js";
|
|
2
2
|
import type { TargetRef } from "./workflow.js";
|
|
3
3
|
|
|
4
|
-
export type NotificationStatus = "published" | "archived";
|
|
4
|
+
export type NotificationStatus = "scheduled" | "published" | "archived";
|
|
5
|
+
export type ExternalNotificationChannel = "email" | "sms";
|
|
6
|
+
export type NotificationDeliveryStatus =
|
|
7
|
+
| "pending"
|
|
8
|
+
| "processing"
|
|
9
|
+
| "retrying"
|
|
10
|
+
| "delivered"
|
|
11
|
+
| "dead";
|
|
5
12
|
|
|
6
13
|
export interface PlatformNotification {
|
|
7
14
|
id: string;
|
|
@@ -11,6 +18,7 @@ export interface PlatformNotification {
|
|
|
11
18
|
category: string;
|
|
12
19
|
status: NotificationStatus;
|
|
13
20
|
publishedAt: ISODateTime;
|
|
21
|
+
scheduledFor?: ISODateTime;
|
|
14
22
|
createdAt: ISODateTime;
|
|
15
23
|
createdBy: string;
|
|
16
24
|
updatedAt: ISODateTime;
|
|
@@ -19,13 +27,29 @@ export interface PlatformNotification {
|
|
|
19
27
|
archivedBy?: string;
|
|
20
28
|
target?: TargetRef;
|
|
21
29
|
recipientUserId?: string;
|
|
30
|
+
recipientUserIds?: string[];
|
|
22
31
|
recipientRoleCodes?: string[];
|
|
32
|
+
recipientDepartmentIds?: string[];
|
|
33
|
+
includeChildDepartments?: boolean;
|
|
34
|
+
deliveryChannels?: ExternalNotificationChannel[];
|
|
23
35
|
sourceKey?: string;
|
|
36
|
+
revision: number;
|
|
24
37
|
}
|
|
25
38
|
|
|
26
39
|
export interface NotificationInboxItem extends PlatformNotification {
|
|
27
40
|
read: boolean;
|
|
28
41
|
readAt?: ISODateTime;
|
|
42
|
+
favorite: boolean;
|
|
43
|
+
favoritedAt?: ISODateTime;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type NotificationReadFilter = "read" | "unread";
|
|
47
|
+
|
|
48
|
+
export interface NotificationInboxFilters {
|
|
49
|
+
q?: string;
|
|
50
|
+
category?: string;
|
|
51
|
+
read?: NotificationReadFilter;
|
|
52
|
+
favorite?: boolean;
|
|
29
53
|
}
|
|
30
54
|
|
|
31
55
|
export interface PublishNotificationInput {
|
|
@@ -35,12 +59,79 @@ export interface PublishNotificationInput {
|
|
|
35
59
|
category: string;
|
|
36
60
|
target?: TargetRef;
|
|
37
61
|
recipientUserId?: string;
|
|
62
|
+
recipientUserIds?: string[];
|
|
38
63
|
recipientRoleCodes?: string[];
|
|
64
|
+
recipientDepartmentIds?: string[];
|
|
65
|
+
includeChildDepartments?: boolean;
|
|
66
|
+
deliveryChannels?: ExternalNotificationChannel[];
|
|
67
|
+
scheduledFor?: ISODateTime;
|
|
39
68
|
sourceKey?: string;
|
|
40
69
|
}
|
|
41
70
|
|
|
71
|
+
export interface NotificationPreference {
|
|
72
|
+
userId: string;
|
|
73
|
+
enabledChannels: ExternalNotificationChannel[];
|
|
74
|
+
disabledCategories: string[];
|
|
75
|
+
updatedAt?: ISODateTime;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type UpdateNotificationPreferenceInput = Pick<
|
|
79
|
+
NotificationPreference,
|
|
80
|
+
"enabledChannels" | "disabledCategories"
|
|
81
|
+
>;
|
|
82
|
+
|
|
83
|
+
export interface NotificationAudienceOption {
|
|
84
|
+
id: string;
|
|
85
|
+
label: string;
|
|
86
|
+
code?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface NotificationAudienceOptions {
|
|
90
|
+
users: NotificationAudienceOption[];
|
|
91
|
+
roles: NotificationAudienceOption[];
|
|
92
|
+
departments: NotificationAudienceOption[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface NotificationDelivery {
|
|
96
|
+
id: string;
|
|
97
|
+
notificationId: string;
|
|
98
|
+
userId: string;
|
|
99
|
+
channel: ExternalNotificationChannel;
|
|
100
|
+
status: NotificationDeliveryStatus;
|
|
101
|
+
attemptCount: number;
|
|
102
|
+
nextAttemptAt: ISODateTime;
|
|
103
|
+
deliveredAt?: ISODateTime;
|
|
104
|
+
lastErrorCode?: string;
|
|
105
|
+
createdAt: ISODateTime;
|
|
106
|
+
updatedAt: ISODateTime;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type UpdateNotificationInput = Pick<
|
|
110
|
+
PublishNotificationInput,
|
|
111
|
+
"title" | "summary" | "content" | "category"
|
|
112
|
+
>;
|
|
113
|
+
|
|
114
|
+
export interface NotificationRevision extends UpdateNotificationInput {
|
|
115
|
+
notificationId: string;
|
|
116
|
+
revision: number;
|
|
117
|
+
editedAt: ISODateTime;
|
|
118
|
+
editedBy: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface UpdateNotificationResult {
|
|
122
|
+
notification: PlatformNotification;
|
|
123
|
+
changed: boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
42
126
|
export interface MarkNotificationReadResult {
|
|
43
127
|
success: true;
|
|
44
128
|
alreadyRead: boolean;
|
|
45
129
|
readAt: ISODateTime;
|
|
46
130
|
}
|
|
131
|
+
|
|
132
|
+
export interface SetNotificationFavoriteResult {
|
|
133
|
+
success: true;
|
|
134
|
+
favorite: boolean;
|
|
135
|
+
changed: boolean;
|
|
136
|
+
favoritedAt?: ISODateTime;
|
|
137
|
+
}
|
package/src/password.ts
CHANGED
package/src/platform-settings.ts
CHANGED
|
@@ -2,4 +2,35 @@ export interface PlatformBrandingSettings {
|
|
|
2
2
|
softwareName: string;
|
|
3
3
|
logoUrl?: string;
|
|
4
4
|
description?: string;
|
|
5
|
+
/** Runtime display timezone. It is managed by platform.runtime, not branding storage. */
|
|
6
|
+
timeZone?: string;
|
|
5
7
|
}
|
|
8
|
+
|
|
9
|
+
export const WATERMARK_DENSITIES = ["low", "medium", "high"] as const;
|
|
10
|
+
export type WatermarkDensity = (typeof WATERMARK_DENSITIES)[number];
|
|
11
|
+
|
|
12
|
+
export const WATERMARK_CONTENT_FIELDS = [
|
|
13
|
+
"username",
|
|
14
|
+
"displayName",
|
|
15
|
+
"phone",
|
|
16
|
+
"email",
|
|
17
|
+
"custom",
|
|
18
|
+
] as const;
|
|
19
|
+
export type WatermarkContentField = (typeof WATERMARK_CONTENT_FIELDS)[number];
|
|
20
|
+
|
|
21
|
+
export interface PlatformWatermarkPolicy {
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
businessPagesEnabled: boolean;
|
|
24
|
+
density: WatermarkDensity;
|
|
25
|
+
contentFields: WatermarkContentField[];
|
|
26
|
+
customContent: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const DEFAULT_PLATFORM_WATERMARK_POLICY: Readonly<PlatformWatermarkPolicy> =
|
|
30
|
+
{
|
|
31
|
+
enabled: true,
|
|
32
|
+
businessPagesEnabled: true,
|
|
33
|
+
density: "medium",
|
|
34
|
+
contentFields: ["username", "displayName"],
|
|
35
|
+
customContent: "",
|
|
36
|
+
};
|