@zhin.js/plugin-runtime 1.1.18 → 1.1.20
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/lib/config-update-declaration.d.ts +5 -0
- package/lib/config-update-declaration.js +15 -0
- package/lib/database-host.d.ts +9 -0
- package/lib/database-host.js +24 -1
- package/lib/outbound-host.d.ts +21 -0
- package/lib/plugin.d.ts +5 -0
- package/lib/plugin.js +3 -0
- package/lib/snapshot.d.ts +2 -0
- package/lib/snapshot.js +4 -2
- package/lib/token.d.ts +2 -0
- package/lib/token.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export interface PluginConfigUpdateDeclaration {
|
|
2
|
+
readonly featurePaths: readonly (readonly string[])[];
|
|
3
|
+
}
|
|
4
|
+
/** @internal Normalize inert ownership metadata at authoring and snapshot boundaries. */
|
|
5
|
+
export declare function freezePluginConfigUpdate(value: PluginConfigUpdateDeclaration): PluginConfigUpdateDeclaration;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** @internal Normalize inert ownership metadata at authoring and snapshot boundaries. */
|
|
2
|
+
export function freezePluginConfigUpdate(value) {
|
|
3
|
+
if (!value || typeof value !== 'object' || Object.keys(value).some(key => key !== 'featurePaths') || !Array.isArray(value.featurePaths)) {
|
|
4
|
+
throw new TypeError('Plugin configUpdate must declare featurePaths');
|
|
5
|
+
}
|
|
6
|
+
return Object.freeze({ featurePaths: Object.freeze(Array.from(value.featurePaths, (path) => {
|
|
7
|
+
if (!Array.isArray(path) || !path.length)
|
|
8
|
+
throw new TypeError('Plugin configUpdate paths must be safe nonempty paths');
|
|
9
|
+
const parts = Array.from(path);
|
|
10
|
+
if (parts.some(part => typeof part !== 'string' || !part || ['__proto__', 'prototype', 'constructor'].includes(part))) {
|
|
11
|
+
throw new TypeError('Plugin configUpdate paths must be safe nonempty paths');
|
|
12
|
+
}
|
|
13
|
+
return Object.freeze(parts);
|
|
14
|
+
})) });
|
|
15
|
+
}
|
package/lib/database-host.d.ts
CHANGED
|
@@ -9,6 +9,13 @@ export interface DatabaseHostSelection {
|
|
|
9
9
|
limit(count: number): DatabaseHostSelection;
|
|
10
10
|
then<TResult1 = Record<string, unknown>[], TResult2 = never>(onfulfilled?: ((value: Record<string, unknown>[]) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
|
|
11
11
|
}
|
|
12
|
+
/** Fixed-table read-only grouped aggregates; no SQL, joins or table selection. */
|
|
13
|
+
export interface DatabaseHostAggregation extends PromiseLike<Record<string, unknown>[]> {
|
|
14
|
+
min(field: string, alias?: string): DatabaseHostAggregation;
|
|
15
|
+
max(field: string, alias?: string): DatabaseHostAggregation;
|
|
16
|
+
groupBy(...fields: string[]): DatabaseHostAggregation;
|
|
17
|
+
where(query: Record<string, unknown>): DatabaseHostAggregation;
|
|
18
|
+
}
|
|
12
19
|
/**
|
|
13
20
|
* select() 需要显式列名:'*' 在 SQL 方言里会被当成字面列名(no such column: "*"),
|
|
14
21
|
* 类型层直接拒绝;运行时另有可读报错兜底(见 Host 实现)。
|
|
@@ -21,6 +28,8 @@ type SelectFields<Fields extends readonly string[]> = {
|
|
|
21
28
|
* and the Host-backed `@zhin.js/database` adapter.
|
|
22
29
|
*/
|
|
23
30
|
export interface DatabaseHostModel {
|
|
31
|
+
/** Optional SQL capability; lightweight and transaction models may omit it. */
|
|
32
|
+
aggregate?(): DatabaseHostAggregation;
|
|
24
33
|
select<Fields extends string[]>(...fields: SelectFields<Fields>): DatabaseHostSelection;
|
|
25
34
|
insert(row: Record<string, unknown>): Promise<unknown>;
|
|
26
35
|
delete(): {
|
package/lib/database-host.js
CHANGED
|
@@ -99,8 +99,22 @@ export class PluginDatabaseHost extends OwnerScopedResourceHost {
|
|
|
99
99
|
};
|
|
100
100
|
return Object.freeze(chain);
|
|
101
101
|
}
|
|
102
|
+
#bindAggregation(aggregation) {
|
|
103
|
+
const modify = (operation) => this.#operation.setup(() => {
|
|
104
|
+
const next = operation();
|
|
105
|
+
return next === aggregation ? chain : this.#bindAggregation(next);
|
|
106
|
+
});
|
|
107
|
+
const chain = {
|
|
108
|
+
min: (field, alias) => modify(() => aggregation.min(field, alias)),
|
|
109
|
+
max: (field, alias) => modify(() => aggregation.max(field, alias)),
|
|
110
|
+
groupBy: (...fields) => modify(() => aggregation.groupBy(...fields)),
|
|
111
|
+
where: query => modify(() => aggregation.where(query)),
|
|
112
|
+
then: (fulfilled, rejected) => this.#execute(() => Promise.resolve(aggregation)).then(fulfilled, rejected),
|
|
113
|
+
};
|
|
114
|
+
return Object.freeze(chain);
|
|
115
|
+
}
|
|
102
116
|
#bindModel(model) {
|
|
103
|
-
const count = model.count?.bind(model);
|
|
117
|
+
const count = model.count?.bind(model), aggregate = model.aggregate?.bind(model);
|
|
104
118
|
return Object.freeze({
|
|
105
119
|
select: (...fields) => this.#operation.setup(() => this.#bindSelection(model.select(...fields))),
|
|
106
120
|
insert: (row) => this.#execute(() => model.insert(row)),
|
|
@@ -111,6 +125,7 @@ export class PluginDatabaseHost extends OwnerScopedResourceHost {
|
|
|
111
125
|
where: (query) => this.#execute(() => model.update(patch).where(query)),
|
|
112
126
|
})),
|
|
113
127
|
...(count ? { count: (query) => this.#execute(() => count(query)) } : {}),
|
|
128
|
+
...(aggregate ? { aggregate: () => this.#operation.setup(() => this.#bindAggregation(aggregate())) } : {}),
|
|
114
129
|
});
|
|
115
130
|
}
|
|
116
131
|
async #execute(operation) {
|
|
@@ -125,8 +140,16 @@ export function guardTransactionModel(model, check) {
|
|
|
125
140
|
limit(count) { check(); return selection(source.limit(count)); },
|
|
126
141
|
then(resolve, reject) { return Promise.resolve().then(() => { check(); return source; }).then(resolve, reject); },
|
|
127
142
|
});
|
|
143
|
+
const aggregation = (source) => ({
|
|
144
|
+
min(field, alias) { check(); return aggregation(source.min(field, alias)); },
|
|
145
|
+
max(field, alias) { check(); return aggregation(source.max(field, alias)); },
|
|
146
|
+
groupBy(...fields) { check(); return aggregation(source.groupBy(...fields)); },
|
|
147
|
+
where(query) { check(); return aggregation(source.where(query)); },
|
|
148
|
+
then(resolve, reject) { return Promise.resolve().then(() => { check(); return source; }).then(resolve, reject); },
|
|
149
|
+
});
|
|
128
150
|
return {
|
|
129
151
|
select: (...fields) => { check(); return selection(model.select(...fields)); },
|
|
152
|
+
...(model.aggregate ? { aggregate: () => { check(); return aggregation(model.aggregate()); } } : {}),
|
|
130
153
|
insert: async (row) => { check(); return model.insert(row); },
|
|
131
154
|
update: patch => { check(); return { where: async (query) => { check(); return model.update(patch).where(query); } }; },
|
|
132
155
|
delete: () => { check(); return { where: async (query) => { check(); return model.delete().where(query); } }; },
|
package/lib/outbound-host.d.ts
CHANGED
|
@@ -47,8 +47,29 @@ export interface OutboundEndpointInput {
|
|
|
47
47
|
readonly adapter: string;
|
|
48
48
|
readonly endpointKey: string;
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Per-conversation-kind view of an Endpoint's capability profile (ADR 0044). Structurally the same as
|
|
52
|
+
* `EndpointKindCapabilities` in `@zhin.js/im-contract`; a missing kind or field means "not supported".
|
|
53
|
+
*/
|
|
54
|
+
export interface OutboundKindCapabilities {
|
|
55
|
+
readonly feedback?: Readonly<{
|
|
56
|
+
mode: 'typing' | 'reaction' | 'status-message' | 'none';
|
|
57
|
+
emoji?: string;
|
|
58
|
+
text?: string;
|
|
59
|
+
refreshSeconds?: number;
|
|
60
|
+
}>;
|
|
61
|
+
readonly reaction: boolean;
|
|
62
|
+
readonly edit: boolean;
|
|
63
|
+
readonly recall: boolean;
|
|
64
|
+
readonly spool: boolean;
|
|
65
|
+
/** Messages here must quote an inbound message. */
|
|
66
|
+
readonly needsReply: boolean;
|
|
67
|
+
readonly groupInfo: boolean;
|
|
68
|
+
readonly memberProfile: boolean;
|
|
69
|
+
}
|
|
50
70
|
export interface OutboundEndpointCapabilities {
|
|
51
71
|
readonly operations: readonly OutboundEndpointOperation[];
|
|
72
|
+
readonly kinds?: Readonly<Partial<Record<'private' | 'group' | 'channel', OutboundKindCapabilities>>>;
|
|
52
73
|
}
|
|
53
74
|
export interface OutboundEditInput {
|
|
54
75
|
readonly message: OutboundMessage;
|
package/lib/plugin.d.ts
CHANGED
|
@@ -7,6 +7,9 @@ import type { GenerationHandoffRegistry } from './handoff.js';
|
|
|
7
7
|
import type { FeatureId, PluginId } from './identity.js';
|
|
8
8
|
import type { Scope, Token } from './token.js';
|
|
9
9
|
import type { PluginEventSubscriptions } from './plugin-events.js';
|
|
10
|
+
import { type PluginConfigUpdateDeclaration } from './config-update-declaration.js';
|
|
11
|
+
export { freezePluginConfigUpdate } from './config-update-declaration.js';
|
|
12
|
+
export type { PluginConfigUpdateDeclaration } from './config-update-declaration.js';
|
|
10
13
|
export interface PluginMetadata {
|
|
11
14
|
readonly displayName?: string;
|
|
12
15
|
readonly icon?: string;
|
|
@@ -48,6 +51,8 @@ export interface PluginDefinition<TConfig = unknown> {
|
|
|
48
51
|
readonly config?: PluginConfigSchema<TConfig>;
|
|
49
52
|
readonly metadata?: PluginMetadata;
|
|
50
53
|
readonly requires?: readonly Token<unknown>[];
|
|
54
|
+
/** Fields owned by Features; setup must not capture these as mutable business configuration. */
|
|
55
|
+
readonly configUpdate?: PluginConfigUpdateDeclaration;
|
|
51
56
|
setup?(context: PluginSetupContext<TConfig>): void | Dispose | Promise<void | Dispose>;
|
|
52
57
|
}
|
|
53
58
|
/**
|
package/lib/plugin.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { freezePluginConfigUpdate } from './config-update-declaration.js';
|
|
2
|
+
export { freezePluginConfigUpdate } from './config-update-declaration.js';
|
|
1
3
|
const pluginNamePattern = /^[a-z][a-z0-9-]*$/;
|
|
2
4
|
/**
|
|
3
5
|
* Define the canonical `plugin.ts` entry point.
|
|
@@ -28,6 +30,7 @@ export function definePlugin(definition) {
|
|
|
28
30
|
...definition,
|
|
29
31
|
metadata: definition.metadata ? Object.freeze({ ...definition.metadata }) : undefined,
|
|
30
32
|
requires: Object.freeze([...(definition.requires ?? [])]),
|
|
33
|
+
...(definition.configUpdate !== undefined ? { configUpdate: freezePluginConfigUpdate(definition.configUpdate) } : {}),
|
|
31
34
|
});
|
|
32
35
|
}
|
|
33
36
|
// The application facade exposes these Host contracts beside definePlugin.
|
package/lib/snapshot.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { Dispose } from './dispose.js';
|
|
|
4
4
|
import type { GenerationHandoff } from './handoff.js';
|
|
5
5
|
import type { CapabilityId, FeatureId, PluginId, TokenId } from './identity.js';
|
|
6
6
|
import type { PluginMetadata } from './plugin.js';
|
|
7
|
+
import { type PluginConfigUpdateDeclaration } from './config-update-declaration.js';
|
|
7
8
|
export interface PluginNodeSnapshot {
|
|
8
9
|
readonly id: PluginId;
|
|
9
10
|
readonly instanceKey: string;
|
|
@@ -16,6 +17,7 @@ export interface PluginNodeSnapshot {
|
|
|
16
17
|
readonly metadata?: PluginMetadata;
|
|
17
18
|
/** Immutable schema declaration belonging to this committed generation. */
|
|
18
19
|
readonly configSchema?: Readonly<Record<string, unknown>>;
|
|
20
|
+
readonly configUpdate?: PluginConfigUpdateDeclaration;
|
|
19
21
|
}
|
|
20
22
|
export interface RuntimeSnapshot {
|
|
21
23
|
readonly generation: number;
|
package/lib/snapshot.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { collectGenerationAdmissions, replaceGenerationAdmissions, } from './admission.js';
|
|
2
|
+
import { freezePluginConfigUpdate } from './config-update-declaration.js';
|
|
2
3
|
export class GenerationConflictError extends Error {
|
|
3
4
|
expected;
|
|
4
5
|
actual;
|
|
@@ -214,8 +215,9 @@ export function createSnapshotView(generation, state) {
|
|
|
214
215
|
return Object.freeze({
|
|
215
216
|
...state,
|
|
216
217
|
generation,
|
|
217
|
-
tree: readonlyMap([...state.tree].map(([owner, node]) => [owner, node.configSchema
|
|
218
|
-
? Object.freeze({ ...node, configSchema: freezeSchema(structuredClone(node.configSchema)) })
|
|
218
|
+
tree: readonlyMap([...state.tree].map(([owner, node]) => [owner, node.configSchema || node.configUpdate
|
|
219
|
+
? Object.freeze({ ...node, ...(node.configSchema ? { configSchema: freezeSchema(structuredClone(node.configSchema)) } : {}),
|
|
220
|
+
...(node.configUpdate ? { configUpdate: freezePluginConfigUpdate(node.configUpdate) } : {}) })
|
|
219
221
|
: node])),
|
|
220
222
|
config: readonlyMap(state.config),
|
|
221
223
|
resources: readonlyMap([...state.resources].map(([owner, resources]) => [owner, readonlyMap(resources)])),
|
package/lib/token.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface Token<T> {
|
|
|
7
7
|
}
|
|
8
8
|
export declare function createToken<T>(id: string, description?: string): Token<T>;
|
|
9
9
|
export declare function createRootPrivateToken<T>(id: string, description?: string): Token<T>;
|
|
10
|
+
/** @internal Verify token identity before accepting a generation-private binding. */
|
|
11
|
+
export declare function isRootPrivateToken(token: Token<unknown>): boolean;
|
|
10
12
|
/** A Root-private capability readable only while projecting its owning Feature. */
|
|
11
13
|
export declare function createFeaturePrivateToken<T>(id: string, feature: FeatureId, description?: string): Token<T>;
|
|
12
14
|
/** @internal The actual candidate Root Scope never enters the Feature context. */
|
package/lib/token.js
CHANGED
|
@@ -10,6 +10,8 @@ export function createRootPrivateToken(id, description) {
|
|
|
10
10
|
rootPrivateTokens.add(token);
|
|
11
11
|
return Object.freeze(token);
|
|
12
12
|
}
|
|
13
|
+
/** @internal Verify token identity before accepting a generation-private binding. */
|
|
14
|
+
export function isRootPrivateToken(token) { return rootPrivateTokens.has(token); }
|
|
13
15
|
/** A Root-private capability readable only while projecting its owning Feature. */
|
|
14
16
|
export function createFeaturePrivateToken(id, feature, description) {
|
|
15
17
|
const token = createRootPrivateToken(id, description);
|