@carlonicora/nestjs-neo4jsonapi 4.2.0 → 4.3.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/dist/agents/graph/graph.module.d.ts.map +1 -1
- package/dist/agents/graph/graph.module.js +6 -0
- package/dist/agents/graph/graph.module.js.map +1 -1
- package/dist/agents/graph/interfaces/graph.catalog.interface.d.ts +46 -1
- package/dist/agents/graph/interfaces/graph.catalog.interface.d.ts.map +1 -1
- package/dist/agents/graph/services/graph.catalog.service.d.ts +14 -2
- package/dist/agents/graph/services/graph.catalog.service.d.ts.map +1 -1
- package/dist/agents/graph/services/graph.catalog.service.js +75 -0
- package/dist/agents/graph/services/graph.catalog.service.js.map +1 -1
- package/dist/agents/graph/services/richtext.write.d.ts +52 -0
- package/dist/agents/graph/services/richtext.write.d.ts.map +1 -0
- package/dist/agents/graph/services/richtext.write.js +92 -0
- package/dist/agents/graph/services/richtext.write.js.map +1 -0
- package/dist/agents/graph/services/writable.rules.d.ts +37 -0
- package/dist/agents/graph/services/writable.rules.d.ts.map +1 -0
- package/dist/agents/graph/services/writable.rules.js +46 -0
- package/dist/agents/graph/services/writable.rules.js.map +1 -0
- package/dist/agents/graph/tools/describe-entity.tool.d.ts.map +1 -1
- package/dist/agents/graph/tools/describe-entity.tool.js +15 -0
- package/dist/agents/graph/tools/describe-entity.tool.js.map +1 -1
- package/dist/agents/index.d.ts +1 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/operator/contexts/operator.context.d.ts +17 -1
- package/dist/agents/operator/contexts/operator.context.d.ts.map +1 -1
- package/dist/agents/operator/contexts/operator.context.js +8 -0
- package/dist/agents/operator/contexts/operator.context.js.map +1 -1
- package/dist/agents/operator/controllers/operator.controller.d.ts.map +1 -1
- package/dist/agents/operator/controllers/operator.controller.js +10 -1
- package/dist/agents/operator/controllers/operator.controller.js.map +1 -1
- package/dist/agents/operator/interfaces/operator.tool.interface.d.ts +49 -1
- package/dist/agents/operator/interfaces/operator.tool.interface.d.ts.map +1 -1
- package/dist/agents/operator/interfaces/operator.tool.interface.js.map +1 -1
- package/dist/agents/operator/prompts/operator.system.prompt.d.ts +1 -1
- package/dist/agents/operator/prompts/operator.system.prompt.d.ts.map +1 -1
- package/dist/agents/operator/prompts/operator.system.prompt.js +2 -0
- package/dist/agents/operator/prompts/operator.system.prompt.js.map +1 -1
- package/dist/agents/operator/services/operator.service.d.ts +32 -0
- package/dist/agents/operator/services/operator.service.d.ts.map +1 -1
- package/dist/agents/operator/services/operator.service.js +83 -2
- package/dist/agents/operator/services/operator.service.js.map +1 -1
- package/dist/agents/operator/tools/entity-write.tools.d.ts +75 -19
- package/dist/agents/operator/tools/entity-write.tools.d.ts.map +1 -1
- package/dist/agents/operator/tools/entity-write.tools.js +422 -149
- package/dist/agents/operator/tools/entity-write.tools.js.map +1 -1
- package/dist/common/helpers/bound-content.d.ts +21 -0
- package/dist/common/helpers/bound-content.d.ts.map +1 -0
- package/dist/common/helpers/bound-content.js +28 -0
- package/dist/common/helpers/bound-content.js.map +1 -0
- package/dist/common/helpers/index.d.ts +1 -0
- package/dist/common/helpers/index.d.ts.map +1 -1
- package/dist/common/helpers/index.js +1 -0
- package/dist/common/helpers/index.js.map +1 -1
- package/dist/common/interfaces/entity.schema.interface.d.ts +27 -4
- package/dist/common/interfaces/entity.schema.interface.d.ts.map +1 -1
- package/dist/foundations/assistant/controllers/assistant.controller.d.ts +0 -7
- package/dist/foundations/assistant/controllers/assistant.controller.d.ts.map +1 -1
- package/dist/foundations/assistant/controllers/assistant.controller.js +2 -16
- package/dist/foundations/assistant/controllers/assistant.controller.js.map +1 -1
- package/dist/foundations/assistant/services/assistant.service.d.ts +0 -9
- package/dist/foundations/assistant/services/assistant.service.d.ts.map +1 -1
- package/dist/foundations/assistant/services/assistant.service.js +32 -35
- package/dist/foundations/assistant/services/assistant.service.js.map +1 -1
- package/dist/foundations/assistant-action/entities/assistant-action.d.ts +1 -0
- package/dist/foundations/assistant-action/entities/assistant-action.d.ts.map +1 -1
- package/dist/foundations/assistant-action/entities/assistant-action.js +1 -0
- package/dist/foundations/assistant-action/entities/assistant-action.js.map +1 -1
- package/dist/foundations/assistant-action/services/assistant-action.service.d.ts +2 -0
- package/dist/foundations/assistant-action/services/assistant-action.service.d.ts.map +1 -1
- package/dist/foundations/assistant-action/services/assistant-action.service.js +1 -0
- package/dist/foundations/assistant-action/services/assistant-action.service.js.map +1 -1
- package/dist/mcp/mcp.module.d.ts +4 -0
- package/dist/mcp/mcp.module.d.ts.map +1 -1
- package/dist/mcp/mcp.module.js +6 -1
- package/dist/mcp/mcp.module.js.map +1 -1
- package/dist/mcp/services/mcp.entity.write.service.d.ts +16 -1
- package/dist/mcp/services/mcp.entity.write.service.d.ts.map +1 -1
- package/dist/mcp/services/mcp.entity.write.service.js +25 -6
- package/dist/mcp/services/mcp.entity.write.service.js.map +1 -1
- package/dist/mcp/services/mcp.promoted.tools.factory.d.ts +21 -4
- package/dist/mcp/services/mcp.promoted.tools.factory.d.ts.map +1 -1
- package/dist/mcp/services/mcp.promoted.tools.factory.js +28 -5
- package/dist/mcp/services/mcp.promoted.tools.factory.js.map +1 -1
- package/package.json +1 -1
|
@@ -16,9 +16,18 @@ const common_1 = require("@nestjs/common");
|
|
|
16
16
|
const node_crypto_1 = require("node:crypto");
|
|
17
17
|
const zod_1 = require("zod");
|
|
18
18
|
const entity_service_registry_1 = require("../../../common/registries/entity.service.registry");
|
|
19
|
+
const blocknote_service_1 = require("../../../core/blocknote/services/blocknote.service");
|
|
19
20
|
const graph_catalog_service_1 = require("../../graph/services/graph.catalog.service");
|
|
21
|
+
const richtext_write_1 = require("../../graph/services/richtext.write");
|
|
20
22
|
const scope_guard_1 = require("../../graph/services/scope.guard");
|
|
23
|
+
const writable_rules_1 = require("../../graph/services/writable.rules");
|
|
21
24
|
const tool_factory_1 = require("../../graph/tools/tool.factory");
|
|
25
|
+
/**
|
|
26
|
+
* Label used whenever a referenced record cannot be resolved to a name — it does
|
|
27
|
+
* not exist, or it sits outside the run's scope. NEVER fall back to the id: the
|
|
28
|
+
* approval card must not leak ids, nor names from another scope.
|
|
29
|
+
*/
|
|
30
|
+
const NOT_FOUND_LABEL = "(not found)";
|
|
22
31
|
const isError = (value) => typeof value === "object" && value !== null && "error" in value;
|
|
23
32
|
/**
|
|
24
33
|
* Generic, catalog-driven create/update/delete/link/unlink tools for the operator.
|
|
@@ -28,14 +37,23 @@ const isError = (value) => typeof value === "object" && value !== null && "error
|
|
|
28
37
|
* 1. **Opt-in.** Only descriptors that declare `chat.writable` are touchable, and
|
|
29
38
|
* only when the caller's modules grant access. When nothing is writable the
|
|
30
39
|
* tools are not built at all, so a host application that never opts in sees
|
|
31
|
-
* exactly the tool set it had before.
|
|
40
|
+
* exactly the tool set it had before. `chat.writable` may narrow further to an
|
|
41
|
+
* allow-list of fields and relationships, which `describe_entity` reports and
|
|
42
|
+
* every check here enforces.
|
|
32
43
|
* 2. **Scoped.** Every id in every payload is checked through `ScopeGuard`, and
|
|
33
44
|
* `create` overwrites the scope relationship with the run's OWN scope id — the
|
|
34
45
|
* model cannot name a different one. The scope relationship itself can never be
|
|
35
46
|
* re-pointed, so a record cannot be moved between scope roots.
|
|
36
|
-
* 3. **Service-only
|
|
37
|
-
*
|
|
38
|
-
*
|
|
47
|
+
* 3. **Service-only, through the same DTO path the controllers use.** Writes go
|
|
48
|
+
* through `EntityServiceRegistry.get(type)`, i.e. the AbstractService, never the
|
|
49
|
+
* repository — and always via its `*FromDTO` helpers with a `JsonApiDTOData`,
|
|
50
|
+
* exactly as an HTTP controller does. That is what makes a host application's
|
|
51
|
+
* OVERRIDE of `createFromDTO` / `patchFromDTO` fire (ownership checks,
|
|
52
|
+
* knowledge-graph and summariser scheduling, cache invalidation) and what lets
|
|
53
|
+
* `mapDTOToParams` fill `contextKey` relationships from CLS. Calling the plain
|
|
54
|
+
* `create` / `patch` would skip every one of those: the record is written, the
|
|
55
|
+
* app's read query for its type then finds nothing (a required owner edge is
|
|
56
|
+
* missing) and the tool reports `not found` for a record that exists. No Cypher
|
|
39
57
|
* is written here.
|
|
40
58
|
*/
|
|
41
59
|
let EntityWriteTools = EntityWriteTools_1 = class EntityWriteTools {
|
|
@@ -46,11 +64,19 @@ let EntityWriteTools = EntityWriteTools_1 = class EntityWriteTools {
|
|
|
46
64
|
* from GraphModule — a missing provider fails loudly at boot rather than
|
|
47
65
|
* silently dropping the per-turn tool-call audit trail.
|
|
48
66
|
*/
|
|
49
|
-
factory
|
|
67
|
+
factory,
|
|
68
|
+
/**
|
|
69
|
+
* Optional in the TYPE signature for the same reason as `factory`, and
|
|
70
|
+
* resolved from GraphModule (which re-exports BlockNoteModule) exactly the
|
|
71
|
+
* same way. Without it a rich-text field would pass through as the model's
|
|
72
|
+
* raw markdown; see `convertRichtextFields`.
|
|
73
|
+
*/
|
|
74
|
+
blockNote) {
|
|
50
75
|
this.catalog = catalog;
|
|
51
76
|
this.registry = registry;
|
|
52
77
|
this.scopeGuard = scopeGuard;
|
|
53
78
|
this.factory = factory;
|
|
79
|
+
this.blockNote = blockNote;
|
|
54
80
|
this.logger = new common_1.Logger(EntityWriteTools_1.name);
|
|
55
81
|
}
|
|
56
82
|
/**
|
|
@@ -70,16 +96,21 @@ let EntityWriteTools = EntityWriteTools_1 = class EntityWriteTools {
|
|
|
70
96
|
type: typeField,
|
|
71
97
|
fields: zod_1.z
|
|
72
98
|
.record(zod_1.z.string(), zod_1.z.any())
|
|
73
|
-
.describe("Field values keyed by field name. Call describe_entity first to learn the valid field names."
|
|
99
|
+
.describe("Field values keyed by field name. Call describe_entity first to learn the valid field names. " +
|
|
100
|
+
"Only fields describe_entity marks writable may be set."),
|
|
74
101
|
relationships: zod_1.z
|
|
75
102
|
.record(zod_1.z.string(), zod_1.z.string())
|
|
76
103
|
.optional()
|
|
77
|
-
.describe("Related record ids keyed by relationship name, for relationships declared on this type."
|
|
104
|
+
.describe("Related record ids keyed by relationship name, for relationships declared on this type. " +
|
|
105
|
+
"Only relationships describe_entity marks writable may be set."),
|
|
78
106
|
});
|
|
79
107
|
const updateSchema = zod_1.z.object({
|
|
80
108
|
type: typeField,
|
|
81
109
|
id: zod_1.z.string().describe("Id of the record to update."),
|
|
82
|
-
fields: zod_1.z
|
|
110
|
+
fields: zod_1.z
|
|
111
|
+
.record(zod_1.z.string(), zod_1.z.any())
|
|
112
|
+
.describe("Only the field values that change. Others are left untouched. " +
|
|
113
|
+
"Only fields describe_entity marks writable may be set."),
|
|
83
114
|
});
|
|
84
115
|
const deleteSchema = zod_1.z.object({
|
|
85
116
|
type: typeField,
|
|
@@ -88,29 +119,42 @@ let EntityWriteTools = EntityWriteTools_1 = class EntityWriteTools {
|
|
|
88
119
|
const linkSchema = zod_1.z.object({
|
|
89
120
|
type: typeField,
|
|
90
121
|
id: zod_1.z.string().describe("Id of the record whose relationship changes."),
|
|
91
|
-
relationship: zod_1.z
|
|
122
|
+
relationship: zod_1.z
|
|
123
|
+
.string()
|
|
124
|
+
.describe("Relationship name as reported by describe_entity. " +
|
|
125
|
+
"Only relationships describe_entity marks writable may be set."),
|
|
92
126
|
targetIds: zod_1.z.array(zod_1.z.string()).describe("Ids of the related records."),
|
|
93
127
|
});
|
|
94
128
|
return [
|
|
95
129
|
{
|
|
96
130
|
tool: new tools_1.DynamicStructuredTool({
|
|
97
131
|
name: "create_entity",
|
|
98
|
-
description: `Creates one new record. Writable types: ${types}.
|
|
132
|
+
description: `Creates one new record. Writable types: ${types}. ` +
|
|
133
|
+
"Only fields describe_entity marks writable may be set, and only relationships describe_entity marks writable. " +
|
|
134
|
+
richtext_write_1.RICHTEXT_HINT +
|
|
135
|
+
" This action requires user approval before it runs.",
|
|
99
136
|
schema: createSchema,
|
|
100
137
|
func: async (input) => JSON.stringify(await this.createEntity(input, ctx, recorder)),
|
|
101
138
|
}),
|
|
102
139
|
destructive: true,
|
|
103
140
|
summarise: (args) => this.summariseCreate(args),
|
|
141
|
+
present: (args) => this.presentCreate(args, ctx),
|
|
142
|
+
validate: async (args) => this.rejection(await this.prepareCreate(args, ctx)),
|
|
104
143
|
},
|
|
105
144
|
{
|
|
106
145
|
tool: new tools_1.DynamicStructuredTool({
|
|
107
146
|
name: "update_entity",
|
|
108
|
-
description: `Updates the given fields of one existing record, leaving every other field untouched. Writable types: ${types}.
|
|
147
|
+
description: `Updates the given fields of one existing record, leaving every other field untouched. Writable types: ${types}. ` +
|
|
148
|
+
"Only fields describe_entity marks writable may be set. " +
|
|
149
|
+
richtext_write_1.RICHTEXT_HINT +
|
|
150
|
+
" This action requires user approval before it runs.",
|
|
109
151
|
schema: updateSchema,
|
|
110
152
|
func: async (input) => JSON.stringify(await this.updateEntity(input, ctx, recorder)),
|
|
111
153
|
}),
|
|
112
154
|
destructive: true,
|
|
113
|
-
summarise: (args) => this.summariseUpdate(args),
|
|
155
|
+
summarise: (args) => this.summariseUpdate(args, ctx),
|
|
156
|
+
present: (args) => this.presentUpdate(args, ctx),
|
|
157
|
+
validate: async (args) => this.rejection(await this.prepareUpdate(args, ctx)),
|
|
114
158
|
},
|
|
115
159
|
{
|
|
116
160
|
tool: new tools_1.DynamicStructuredTool({
|
|
@@ -120,101 +164,91 @@ let EntityWriteTools = EntityWriteTools_1 = class EntityWriteTools {
|
|
|
120
164
|
func: async (input) => JSON.stringify(await this.deleteEntity(input, ctx, recorder)),
|
|
121
165
|
}),
|
|
122
166
|
destructive: true,
|
|
123
|
-
summarise: (args) => this.summariseDelete(args),
|
|
167
|
+
summarise: (args) => this.summariseDelete(args, ctx),
|
|
168
|
+
present: (args) => this.presentDelete(args, ctx),
|
|
169
|
+
validate: async (args) => this.rejection(await this.prepareDelete(args, ctx)),
|
|
124
170
|
},
|
|
125
171
|
{
|
|
126
172
|
tool: new tools_1.DynamicStructuredTool({
|
|
127
173
|
name: "link_entities",
|
|
128
|
-
description: `Adds related records to one relationship of an existing record. Writable types: ${types}.
|
|
174
|
+
description: `Adds related records to one relationship of an existing record. Writable types: ${types}. ` +
|
|
175
|
+
"Only relationships describe_entity marks writable may be set. " +
|
|
176
|
+
"This action requires user approval before it runs.",
|
|
129
177
|
schema: linkSchema,
|
|
130
178
|
func: async (input) => JSON.stringify(await this.linkEntities(input, ctx, recorder)),
|
|
131
179
|
}),
|
|
132
180
|
destructive: true,
|
|
133
|
-
summarise: (args) => this.summariseLink(args, "Link"),
|
|
181
|
+
summarise: (args) => this.summariseLink(args, "Link", ctx),
|
|
182
|
+
present: (args) => this.presentLink(args, ctx),
|
|
183
|
+
validate: async (args) => this.rejection(await this.prepareLink(args, ctx, "link")),
|
|
134
184
|
},
|
|
135
185
|
{
|
|
136
186
|
tool: new tools_1.DynamicStructuredTool({
|
|
137
187
|
name: "unlink_entities",
|
|
138
|
-
description: `Removes related records from one relationship of an existing record. Writable types: ${types}.
|
|
188
|
+
description: `Removes related records from one relationship of an existing record. Writable types: ${types}. ` +
|
|
189
|
+
"Only relationships describe_entity marks writable may be set. " +
|
|
190
|
+
"This action requires user approval before it runs.",
|
|
139
191
|
schema: linkSchema,
|
|
140
192
|
func: async (input) => JSON.stringify(await this.unlinkEntities(input, ctx, recorder)),
|
|
141
193
|
}),
|
|
142
194
|
destructive: true,
|
|
143
|
-
summarise: (args) => this.summariseLink(args, "Unlink"),
|
|
195
|
+
summarise: (args) => this.summariseLink(args, "Unlink", ctx),
|
|
196
|
+
present: (args) => this.presentLink(args, ctx),
|
|
197
|
+
validate: async (args) => this.rejection(await this.prepareLink(args, ctx, "unlink")),
|
|
144
198
|
},
|
|
145
199
|
];
|
|
146
200
|
}
|
|
147
201
|
async createEntity(input, ctx, recorder) {
|
|
148
202
|
return this.capture("create_entity", input, recorder, async () => {
|
|
149
|
-
const
|
|
150
|
-
if (isError(
|
|
151
|
-
return
|
|
152
|
-
const
|
|
153
|
-
const fieldError = this.validateFields(entity, fields);
|
|
154
|
-
if (fieldError)
|
|
155
|
-
return { error: fieldError };
|
|
156
|
-
// The run's scope is authoritative: whatever the model supplied for the
|
|
157
|
-
// scope relationship is discarded before validation and replaced below.
|
|
158
|
-
const scopeKey = this.scopeKeyOf(entity);
|
|
159
|
-
const relationships = { ...(input.relationships ?? {}) };
|
|
160
|
-
if (scopeKey)
|
|
161
|
-
delete relationships[scopeKey];
|
|
162
|
-
const relationshipError = await this.validateRelationships(entity, relationships, ctx);
|
|
163
|
-
if (relationshipError)
|
|
164
|
-
return { error: relationshipError };
|
|
165
|
-
const service = this.registry.get(entity.type);
|
|
166
|
-
if (!service)
|
|
167
|
-
return { error: `Service not available for "${entity.type}".` };
|
|
203
|
+
const prepared = await this.prepareCreate(input, ctx);
|
|
204
|
+
if (isError(prepared))
|
|
205
|
+
return prepared;
|
|
206
|
+
const { entity, service, fields, relationships } = prepared;
|
|
168
207
|
const id = (0, node_crypto_1.randomUUID)();
|
|
169
208
|
return this.dispatch(async () => {
|
|
170
|
-
|
|
209
|
+
// The model writes markdown into a rich-text field, because that is what
|
|
210
|
+
// the read side rendered for it. Storing it verbatim leaves a record the
|
|
211
|
+
// frontend cannot render at all, so it becomes a BlockNote document here
|
|
212
|
+
// — after validation (which checks the markdown is a string) and after
|
|
213
|
+
// the approval card, which shows the model's own text.
|
|
214
|
+
const attributes = await (0, richtext_write_1.convertRichtextFields)(this.blockNote, entity, fields);
|
|
215
|
+
const data = {
|
|
216
|
+
type: entity.type,
|
|
171
217
|
id,
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
});
|
|
218
|
+
attributes,
|
|
219
|
+
relationships: this.createRelationships({ entity, relationships, ctx }),
|
|
220
|
+
};
|
|
221
|
+
await service.createFromDTO({ data });
|
|
176
222
|
return { id, type: entity.type, created: true };
|
|
177
223
|
});
|
|
178
224
|
});
|
|
179
225
|
}
|
|
180
226
|
async updateEntity(input, ctx, recorder) {
|
|
181
227
|
return this.capture("update_entity", input, recorder, async () => {
|
|
182
|
-
const
|
|
183
|
-
if (isError(
|
|
184
|
-
return
|
|
185
|
-
const
|
|
186
|
-
if (outOfScope)
|
|
187
|
-
return outOfScope;
|
|
188
|
-
const fields = input.fields ?? {};
|
|
189
|
-
const fieldError = this.validateFields(entity, fields);
|
|
190
|
-
if (fieldError)
|
|
191
|
-
return { error: fieldError };
|
|
192
|
-
const service = this.registry.get(entity.type);
|
|
193
|
-
if (!service)
|
|
194
|
-
return { error: `Service not available for "${entity.type}".` };
|
|
228
|
+
const prepared = await this.prepareUpdate(input, ctx);
|
|
229
|
+
if (isError(prepared))
|
|
230
|
+
return prepared;
|
|
231
|
+
const { entity, service, fields, id } = prepared;
|
|
195
232
|
return this.dispatch(async () => {
|
|
233
|
+
// Markdown in a rich-text field becomes a BlockNote document; see createEntity.
|
|
234
|
+
const attributes = await (0, richtext_write_1.convertRichtextFields)(this.blockNote, entity, fields);
|
|
196
235
|
// patch, not put: only the named fields change. put would map every
|
|
197
236
|
// descriptor relationship and treat the ones absent from the payload as
|
|
198
237
|
// "delete all edges", silently stripping the record's relationships.
|
|
199
|
-
await service.
|
|
200
|
-
return { id
|
|
238
|
+
await service.patchFromDTO({ data: { type: entity.type, id, attributes } });
|
|
239
|
+
return { id, type: entity.type, updated: true };
|
|
201
240
|
});
|
|
202
241
|
});
|
|
203
242
|
}
|
|
204
243
|
async deleteEntity(input, ctx, recorder) {
|
|
205
244
|
return this.capture("delete_entity", input, recorder, async () => {
|
|
206
|
-
const
|
|
207
|
-
if (isError(
|
|
208
|
-
return
|
|
209
|
-
const
|
|
210
|
-
if (outOfScope)
|
|
211
|
-
return outOfScope;
|
|
212
|
-
const service = this.registry.get(entity.type);
|
|
213
|
-
if (!service)
|
|
214
|
-
return { error: `Service not available for "${entity.type}".` };
|
|
245
|
+
const prepared = await this.prepareDelete(input, ctx);
|
|
246
|
+
if (isError(prepared))
|
|
247
|
+
return prepared;
|
|
248
|
+
const { entity, service, id } = prepared;
|
|
215
249
|
return this.dispatch(async () => {
|
|
216
|
-
await service.delete({ id
|
|
217
|
-
return { id
|
|
250
|
+
await service.delete({ id });
|
|
251
|
+
return { id, type: entity.type, deleted: true };
|
|
218
252
|
});
|
|
219
253
|
});
|
|
220
254
|
}
|
|
@@ -225,6 +259,115 @@ let EntityWriteTools = EntityWriteTools_1 = class EntityWriteTools {
|
|
|
225
259
|
return this.capture("unlink_entities", input, recorder, () => this.applyLink(input, ctx, "unlink"));
|
|
226
260
|
}
|
|
227
261
|
// ---------------------------------------------------------------------------
|
|
262
|
+
// Preparation
|
|
263
|
+
//
|
|
264
|
+
// Every check a write performs lives here, once per tool, so the `validate`
|
|
265
|
+
// hook — which runs BEFORE the user is asked to approve anything — rejects
|
|
266
|
+
// exactly the calls the execution path rejects, with exactly the same message.
|
|
267
|
+
// A prepared result carries the payload the write then uses, so a check never
|
|
268
|
+
// has to be repeated at write time.
|
|
269
|
+
//
|
|
270
|
+
// Tool args reach `validate` straight from the model, unvalidated by the zod
|
|
271
|
+
// schema, so every accessor here is defensive about shape.
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
async prepareCreate(input, ctx) {
|
|
274
|
+
const entity = this.resolveWritable(input?.type, ctx);
|
|
275
|
+
if (isError(entity))
|
|
276
|
+
return entity;
|
|
277
|
+
const fields = this.toValueMap(input?.fields);
|
|
278
|
+
const fieldError = this.validateFields(entity, fields);
|
|
279
|
+
if (fieldError)
|
|
280
|
+
return { error: fieldError };
|
|
281
|
+
// The run's scope is authoritative: whatever the model supplied for the scope
|
|
282
|
+
// relationship is discarded here and replaced at write time.
|
|
283
|
+
const scopeKey = (0, writable_rules_1.scopeKeyOf)(entity);
|
|
284
|
+
const relationships = this.toValueMap(input?.relationships);
|
|
285
|
+
if (scopeKey)
|
|
286
|
+
delete relationships[scopeKey];
|
|
287
|
+
const relationshipError = await this.validateRelationships(entity, relationships, ctx);
|
|
288
|
+
if (relationshipError)
|
|
289
|
+
return { error: relationshipError };
|
|
290
|
+
const service = this.registry.get(entity.type);
|
|
291
|
+
if (!service)
|
|
292
|
+
return { error: `Service not available for "${entity.type}".` };
|
|
293
|
+
return { entity, service, fields, relationships };
|
|
294
|
+
}
|
|
295
|
+
async prepareUpdate(input, ctx) {
|
|
296
|
+
const entity = this.resolveWritable(input?.type, ctx);
|
|
297
|
+
if (isError(entity))
|
|
298
|
+
return entity;
|
|
299
|
+
const id = this.toId(input?.id);
|
|
300
|
+
const outOfScope = await this.requireInScope(entity.type, [id], ctx);
|
|
301
|
+
if (outOfScope)
|
|
302
|
+
return outOfScope;
|
|
303
|
+
const fields = this.toValueMap(input?.fields);
|
|
304
|
+
const fieldError = this.validateFields(entity, fields);
|
|
305
|
+
if (fieldError)
|
|
306
|
+
return { error: fieldError };
|
|
307
|
+
const service = this.registry.get(entity.type);
|
|
308
|
+
if (!service)
|
|
309
|
+
return { error: `Service not available for "${entity.type}".` };
|
|
310
|
+
return { entity, service, id, fields };
|
|
311
|
+
}
|
|
312
|
+
async prepareDelete(input, ctx) {
|
|
313
|
+
const entity = this.resolveWritable(input?.type, ctx);
|
|
314
|
+
if (isError(entity))
|
|
315
|
+
return entity;
|
|
316
|
+
const id = this.toId(input?.id);
|
|
317
|
+
const outOfScope = await this.requireInScope(entity.type, [id], ctx);
|
|
318
|
+
if (outOfScope)
|
|
319
|
+
return outOfScope;
|
|
320
|
+
const service = this.registry.get(entity.type);
|
|
321
|
+
if (!service)
|
|
322
|
+
return { error: `Service not available for "${entity.type}".` };
|
|
323
|
+
return { entity, service, id };
|
|
324
|
+
}
|
|
325
|
+
async prepareLink(input, ctx, mode) {
|
|
326
|
+
const entity = this.resolveWritable(input?.type, ctx);
|
|
327
|
+
if (isError(entity))
|
|
328
|
+
return entity;
|
|
329
|
+
const relationship = entity.relationships.find((candidate) => candidate.name === input?.relationship);
|
|
330
|
+
if (!relationship) {
|
|
331
|
+
return {
|
|
332
|
+
error: `Relationship "${input?.relationship}" is not available on ${entity.type}. Valid relationships: [${entity.relationships
|
|
333
|
+
.map((candidate) => candidate.name)
|
|
334
|
+
.join(", ")}].`,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
const rejection = this.rejectUnwritableRelationship(entity, relationship);
|
|
338
|
+
if (rejection)
|
|
339
|
+
return { error: rejection };
|
|
340
|
+
const targetIds = Array.isArray(input?.targetIds) ? input.targetIds.map((id) => String(id)) : [];
|
|
341
|
+
if (!targetIds.length)
|
|
342
|
+
return { error: "targetIds must contain at least one id." };
|
|
343
|
+
if (relationship.cardinality === "one") {
|
|
344
|
+
if (mode === "unlink") {
|
|
345
|
+
// A to-one edge cannot be removed through the generic patch path: the
|
|
346
|
+
// repository maps an empty list to "no change", so the edge would survive
|
|
347
|
+
// while the tool reported success.
|
|
348
|
+
return {
|
|
349
|
+
error: `Relationship "${relationship.name}" on ${entity.type} holds a single record and cannot be cleared. Link a different record instead.`,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
if (targetIds.length !== 1) {
|
|
353
|
+
return { error: `Relationship "${relationship.name}" on ${entity.type} accepts exactly one record.` };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const id = this.toId(input?.id);
|
|
357
|
+
const outOfScope = (await this.requireInScope(entity.type, [id], ctx)) ??
|
|
358
|
+
(await this.requireInScope(relationship.targetType, targetIds, ctx));
|
|
359
|
+
if (outOfScope)
|
|
360
|
+
return outOfScope;
|
|
361
|
+
const service = this.registry.get(entity.type);
|
|
362
|
+
if (!service)
|
|
363
|
+
return { error: `Service not available for "${entity.type}".` };
|
|
364
|
+
return { entity, service, id, relationship, targetIds };
|
|
365
|
+
}
|
|
366
|
+
/** The `validate` hook's answer: the rejection message, or `null` when the call may proceed. */
|
|
367
|
+
rejection(prepared) {
|
|
368
|
+
return isError(prepared) ? prepared.error : null;
|
|
369
|
+
}
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
228
371
|
// Guards
|
|
229
372
|
// ---------------------------------------------------------------------------
|
|
230
373
|
resolveWritable(type, ctx) {
|
|
@@ -236,7 +379,13 @@ let EntityWriteTools = EntityWriteTools_1 = class EntityWriteTools {
|
|
|
236
379
|
}
|
|
237
380
|
return entity;
|
|
238
381
|
}
|
|
239
|
-
/**
|
|
382
|
+
/**
|
|
383
|
+
* Reject any field the catalog does not declare, any field the descriptor does
|
|
384
|
+
* not open to the assistant, and any type mismatch. The writable list is what
|
|
385
|
+
* keeps the model out of the fields the system generates itself (a tldr, a
|
|
386
|
+
* summary, an ai status): they are described — the model can read them — but
|
|
387
|
+
* writing them would overwrite generated content with a guess.
|
|
388
|
+
*/
|
|
240
389
|
validateFields(entity, fields) {
|
|
241
390
|
const byName = new Map(entity.fields.map((field) => [field.name, field]));
|
|
242
391
|
for (const [name, value] of Object.entries(fields)) {
|
|
@@ -246,6 +395,9 @@ let EntityWriteTools = EntityWriteTools_1 = class EntityWriteTools {
|
|
|
246
395
|
.map((field) => field.name)
|
|
247
396
|
.join(", ")}].`;
|
|
248
397
|
}
|
|
398
|
+
if (!(0, writable_rules_1.isFieldWritable)(entity, name)) {
|
|
399
|
+
return `Field "${name}" on ${entity.type} is not writable. Writable fields: [${(0, writable_rules_1.writableFieldNames)(entity).join(", ")}].`;
|
|
400
|
+
}
|
|
249
401
|
if (!this.matchesType(definition.type, value)) {
|
|
250
402
|
return `Field "${name}" on ${entity.type} expects ${definition.type}.`;
|
|
251
403
|
}
|
|
@@ -277,25 +429,23 @@ let EntityWriteTools = EntityWriteTools_1 = class EntityWriteTools {
|
|
|
277
429
|
return null;
|
|
278
430
|
}
|
|
279
431
|
/**
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
* (`targetType` is the "*" placeholder, which no scope check can honour);
|
|
285
|
-
* - the scope relationship is what confines the record to the run's root, so
|
|
286
|
-
* re-pointing it would move the record into another scope.
|
|
432
|
+
* The message for a relationship the generic write path must never touch. The
|
|
433
|
+
* reasons themselves live in `writable.rules` — the same predicate
|
|
434
|
+
* `describe_entity` uses to tell the model what it may write, so what the model
|
|
435
|
+
* is told and what is enforced here cannot drift.
|
|
287
436
|
*/
|
|
288
437
|
rejectUnwritableRelationship(entity, relationship) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
438
|
+
switch ((0, writable_rules_1.relationshipRejection)(entity, relationship)) {
|
|
439
|
+
case "reverse":
|
|
440
|
+
case "polymorphic":
|
|
441
|
+
return `Relationship "${relationship.name}" on ${entity.type} is read-only and cannot be written.`;
|
|
442
|
+
case "scope":
|
|
443
|
+
return `Relationship "${relationship.name}" on ${entity.type} cannot be changed.`;
|
|
444
|
+
case "notListed":
|
|
445
|
+
return `Relationship "${relationship.name}" on ${entity.type} is not writable. Writable relationships: [${(0, writable_rules_1.writableRelationshipNames)(entity).join(", ")}].`;
|
|
446
|
+
default:
|
|
447
|
+
return null;
|
|
297
448
|
}
|
|
298
|
-
return null;
|
|
299
449
|
}
|
|
300
450
|
/** `null` when every id is inside the run's scope, otherwise the error to return. */
|
|
301
451
|
async requireInScope(type, ids, ctx) {
|
|
@@ -312,12 +462,48 @@ let EntityWriteTools = EntityWriteTools_1 = class EntityWriteTools {
|
|
|
312
462
|
// Internals
|
|
313
463
|
// ---------------------------------------------------------------------------
|
|
314
464
|
/**
|
|
315
|
-
* The
|
|
316
|
-
*
|
|
317
|
-
*
|
|
465
|
+
* The JSON:API `relationships` object a create sends, built from three sources:
|
|
466
|
+
*
|
|
467
|
+
* 1. what the model supplied, keyed by the descriptor's `dtoKey` (NOT the catalog
|
|
468
|
+
* name — `mapDTOToParams` looks relationships up by `dtoKey`, so a payload
|
|
469
|
+
* keyed by name is dropped without a word whenever the two differ);
|
|
470
|
+
* 2. the run's OWN scope root, which overwrites anything the model named — the
|
|
471
|
+
* model's value was already stripped while preparing;
|
|
472
|
+
* 3. the owner, from the run's user. The host application's clients send this on
|
|
473
|
+
* every create, and its read query for the type requires the edge: without it
|
|
474
|
+
* the record is written and then cannot be read back.
|
|
318
475
|
*/
|
|
319
|
-
|
|
320
|
-
|
|
476
|
+
createRelationships(params) {
|
|
477
|
+
const { entity, relationships, ctx } = params;
|
|
478
|
+
const byName = new Map(entity.relationships.map((relationship) => [relationship.name, relationship]));
|
|
479
|
+
const data = {};
|
|
480
|
+
for (const [name, value] of Object.entries(relationships)) {
|
|
481
|
+
const relationship = byName.get(name);
|
|
482
|
+
if (!relationship)
|
|
483
|
+
continue;
|
|
484
|
+
const ids = this.toIdList(value);
|
|
485
|
+
if (!ids.length)
|
|
486
|
+
continue;
|
|
487
|
+
data[relationship.dtoKey] =
|
|
488
|
+
relationship.cardinality === "one"
|
|
489
|
+
? { data: { type: relationship.targetType, id: ids[0] } }
|
|
490
|
+
: { data: ids.map((targetId) => ({ type: relationship.targetType, id: targetId })) };
|
|
491
|
+
}
|
|
492
|
+
const hop = entity.scope?.path[0];
|
|
493
|
+
if (hop && ctx.scopeId)
|
|
494
|
+
data[hop.dtoKey] = { data: { type: hop.targetType, id: ctx.scopeId } };
|
|
495
|
+
if (entity.owner && ctx.userId) {
|
|
496
|
+
data[entity.owner.dtoKey] = { data: { type: entity.owner.type, id: ctx.userId } };
|
|
497
|
+
}
|
|
498
|
+
return data;
|
|
499
|
+
}
|
|
500
|
+
/** A payload map from raw tool args: anything that is not a plain object is empty. */
|
|
501
|
+
toValueMap(value) {
|
|
502
|
+
return value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
|
|
503
|
+
}
|
|
504
|
+
/** An id from raw tool args; a missing one becomes `""`, which no scope check passes. */
|
|
505
|
+
toId(value) {
|
|
506
|
+
return typeof value === "string" ? value : value === null || value === undefined ? "" : String(value);
|
|
321
507
|
}
|
|
322
508
|
writableTypes(ctx) {
|
|
323
509
|
return this.catalog
|
|
@@ -348,64 +534,39 @@ let EntityWriteTools = EntityWriteTools_1 = class EntityWriteTools {
|
|
|
348
534
|
}
|
|
349
535
|
}
|
|
350
536
|
async applyLink(input, ctx, mode) {
|
|
351
|
-
const
|
|
352
|
-
if (isError(
|
|
353
|
-
return
|
|
354
|
-
const
|
|
355
|
-
if (!relationship) {
|
|
356
|
-
return {
|
|
357
|
-
error: `Relationship "${input.relationship}" is not available on ${entity.type}. Valid relationships: [${entity.relationships
|
|
358
|
-
.map((candidate) => candidate.name)
|
|
359
|
-
.join(", ")}].`,
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
const rejection = this.rejectUnwritableRelationship(entity, relationship);
|
|
363
|
-
if (rejection)
|
|
364
|
-
return { error: rejection };
|
|
365
|
-
const targetIds = (input.targetIds ?? []).map((id) => String(id));
|
|
366
|
-
if (!targetIds.length)
|
|
367
|
-
return { error: "targetIds must contain at least one id." };
|
|
368
|
-
if (relationship.cardinality === "one") {
|
|
369
|
-
if (mode === "unlink") {
|
|
370
|
-
// A to-one edge cannot be removed through the generic patch path: the
|
|
371
|
-
// repository maps an empty list to "no change", so the edge would survive
|
|
372
|
-
// while the tool reported success.
|
|
373
|
-
return {
|
|
374
|
-
error: `Relationship "${relationship.name}" on ${entity.type} holds a single record and cannot be cleared. Link a different record instead.`,
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
if (targetIds.length !== 1) {
|
|
378
|
-
return { error: `Relationship "${relationship.name}" on ${entity.type} accepts exactly one record.` };
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
const outOfScope = (await this.requireInScope(entity.type, [input.id], ctx)) ??
|
|
382
|
-
(await this.requireInScope(relationship.targetType, targetIds, ctx));
|
|
383
|
-
if (outOfScope)
|
|
384
|
-
return outOfScope;
|
|
385
|
-
const service = this.registry.get(entity.type);
|
|
386
|
-
if (!service)
|
|
387
|
-
return { error: `Service not available for "${entity.type}".` };
|
|
537
|
+
const prepared = await this.prepareLink(input, ctx, mode);
|
|
538
|
+
if (isError(prepared))
|
|
539
|
+
return prepared;
|
|
540
|
+
const { entity, service, id, relationship, targetIds } = prepared;
|
|
388
541
|
return this.dispatch(async () => {
|
|
389
542
|
if (relationship.cardinality === "one") {
|
|
390
543
|
// The framework's add/remove relationship helpers are to-many only, so a
|
|
391
544
|
// to-one edge is repointed with patch, which replaces the single edge.
|
|
392
|
-
await service.
|
|
545
|
+
await service.patchFromDTO({
|
|
546
|
+
data: {
|
|
547
|
+
type: entity.type,
|
|
548
|
+
id,
|
|
549
|
+
relationships: {
|
|
550
|
+
[relationship.dtoKey]: { data: { type: relationship.targetType, id: targetIds[0] } },
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
});
|
|
393
554
|
}
|
|
394
555
|
else if (mode === "link") {
|
|
395
556
|
await service.addToRelationshipFromDTO({
|
|
396
|
-
id
|
|
557
|
+
id,
|
|
397
558
|
relationship: relationship.name,
|
|
398
|
-
data: targetIds.map((
|
|
559
|
+
data: targetIds.map((targetId) => ({ id: targetId, type: relationship.targetType })),
|
|
399
560
|
});
|
|
400
561
|
}
|
|
401
562
|
else {
|
|
402
563
|
await service.removeFromRelationshipFromDTO({
|
|
403
|
-
id
|
|
564
|
+
id,
|
|
404
565
|
relationship: relationship.name,
|
|
405
|
-
data: targetIds.map((
|
|
566
|
+
data: targetIds.map((targetId) => ({ id: targetId, type: relationship.targetType })),
|
|
406
567
|
});
|
|
407
568
|
}
|
|
408
|
-
return { id
|
|
569
|
+
return { id, type: entity.type, relationship: relationship.name, targetIds, [mode + "ed"]: true };
|
|
409
570
|
});
|
|
410
571
|
}
|
|
411
572
|
/**
|
|
@@ -428,43 +589,154 @@ let EntityWriteTools = EntityWriteTools_1 = class EntityWriteTools {
|
|
|
428
589
|
return this.factory ? this.factory.capture({ tool, input }, fn, recorder) : fn();
|
|
429
590
|
}
|
|
430
591
|
// ---------------------------------------------------------------------------
|
|
592
|
+
// Name resolution
|
|
593
|
+
//
|
|
594
|
+
// The approval card shows NAMES, never ids. Every referenced record is resolved
|
|
595
|
+
// here, through the scope guard first: an id the run may not see resolves to
|
|
596
|
+
// "(not found)" so a cross-scope name can never leak.
|
|
597
|
+
// ---------------------------------------------------------------------------
|
|
598
|
+
async resolveRef(params) {
|
|
599
|
+
const { type, id, ctx } = params;
|
|
600
|
+
const unresolved = { id, type, label: NOT_FOUND_LABEL };
|
|
601
|
+
try {
|
|
602
|
+
if (!(await this.scopeGuard.isInScope({ type, id, ctx })))
|
|
603
|
+
return unresolved;
|
|
604
|
+
const record = await this.registry.get(type)?.findRecordById({ id });
|
|
605
|
+
if (!record)
|
|
606
|
+
return unresolved;
|
|
607
|
+
const catalogEntity = this.catalog.getEntityDetail(type, ctx.userModuleIds);
|
|
608
|
+
const candidate = catalogEntity?.summary?.(record) ??
|
|
609
|
+
record.name ??
|
|
610
|
+
record.title ??
|
|
611
|
+
NOT_FOUND_LABEL;
|
|
612
|
+
const label = typeof candidate === "string" && candidate ? candidate : NOT_FOUND_LABEL;
|
|
613
|
+
return { id, type, label };
|
|
614
|
+
}
|
|
615
|
+
catch (err) {
|
|
616
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
617
|
+
this.logger.warn(`entity-write: could not resolve the name of a ${type} record: ${message}`);
|
|
618
|
+
return unresolved;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
/** Resolve the record the tool call targets, from `args.type` / `args.id`. */
|
|
622
|
+
async resolveTargetRef(args, ctx) {
|
|
623
|
+
const type = this.argType(args);
|
|
624
|
+
const id = typeof args.id === "string" ? args.id : "";
|
|
625
|
+
if (!id)
|
|
626
|
+
return { id: "", type, label: NOT_FOUND_LABEL };
|
|
627
|
+
return this.resolveRef({ type, id, ctx });
|
|
628
|
+
}
|
|
629
|
+
/** Relationship payloads are `Record<string, string>` by schema; arrays are accepted defensively. */
|
|
630
|
+
toIdList(value) {
|
|
631
|
+
if (value === null || value === undefined)
|
|
632
|
+
return [];
|
|
633
|
+
return (Array.isArray(value) ? value : [value]).map((id) => String(id)).filter((id) => id.length > 0);
|
|
634
|
+
}
|
|
635
|
+
/** `"Marcus"` when resolved, bare `(not found)` when not — an id is never rendered. */
|
|
636
|
+
quote(label) {
|
|
637
|
+
return label === NOT_FOUND_LABEL ? NOT_FOUND_LABEL : `"${label}"`;
|
|
638
|
+
}
|
|
639
|
+
// ---------------------------------------------------------------------------
|
|
431
640
|
// Approval-card summaries
|
|
432
641
|
//
|
|
433
|
-
// These are rendered to the user before the action runs.
|
|
434
|
-
//
|
|
642
|
+
// These are rendered to the user before the action runs. Referenced records
|
|
643
|
+
// are named, never identified: no id ever reaches this text.
|
|
435
644
|
// ---------------------------------------------------------------------------
|
|
436
645
|
summariseCreate(args) {
|
|
437
|
-
const type = this.
|
|
646
|
+
const type = this.argType(args);
|
|
438
647
|
const label = this.summariseLabel(args.fields);
|
|
439
648
|
return label ? `Create a new ${type} record named "${label}".` : `Create a new ${type} record.`;
|
|
440
649
|
}
|
|
441
|
-
summariseUpdate(args) {
|
|
442
|
-
const type = this.
|
|
650
|
+
async summariseUpdate(args, ctx) {
|
|
651
|
+
const type = this.argType(args);
|
|
443
652
|
const fields = args.fields && typeof args.fields === "object" ? Object.keys(args.fields) : [];
|
|
444
653
|
const changed = fields.length ? ` (${fields.join(", ")})` : "";
|
|
445
|
-
|
|
654
|
+
const target = await this.resolveTargetRef(args, ctx);
|
|
655
|
+
return `Update the ${type} record ${this.quote(target.label)}${changed}.`;
|
|
446
656
|
}
|
|
447
|
-
summariseDelete(args) {
|
|
448
|
-
|
|
657
|
+
async summariseDelete(args, ctx) {
|
|
658
|
+
const target = await this.resolveTargetRef(args, ctx);
|
|
659
|
+
return `Delete the ${this.argType(args)} record ${this.quote(target.label)}.`;
|
|
449
660
|
}
|
|
450
|
-
summariseLink(args, verb) {
|
|
451
|
-
const
|
|
452
|
-
const relationship = typeof args.relationship === "string" ? args.relationship : "related";
|
|
661
|
+
async summariseLink(args, verb, ctx) {
|
|
662
|
+
const relationship = typeof args.relationship === "string" && args.relationship ? args.relationship : "related";
|
|
453
663
|
const preposition = verb === "Link" ? "to" : "from";
|
|
454
|
-
|
|
664
|
+
const target = await this.resolveTargetRef(args, ctx);
|
|
665
|
+
const targets = await this.resolveLinkTargets(args, ctx);
|
|
666
|
+
const named = targets.length ? targets.map((ref) => this.quote(ref.label)).join(", ") : "no records";
|
|
667
|
+
return `${verb} ${named} ${preposition} the "${relationship}" relationship of the ${this.argType(args)} record ${this.quote(target.label)}.`;
|
|
455
668
|
}
|
|
456
|
-
|
|
669
|
+
argType(args) {
|
|
457
670
|
return typeof args.type === "string" && args.type ? args.type : "record";
|
|
458
671
|
}
|
|
459
|
-
summariseId(args) {
|
|
460
|
-
return typeof args.id === "string" && args.id ? args.id : "(unknown id)";
|
|
461
|
-
}
|
|
462
672
|
summariseLabel(fields) {
|
|
463
673
|
if (!fields || typeof fields !== "object")
|
|
464
674
|
return undefined;
|
|
465
675
|
const candidate = fields.name ?? fields.title;
|
|
466
676
|
return typeof candidate === "string" && candidate ? candidate : undefined;
|
|
467
677
|
}
|
|
678
|
+
// ---------------------------------------------------------------------------
|
|
679
|
+
// Approval-card proposals
|
|
680
|
+
//
|
|
681
|
+
// The structured, name-resolved rendering the card reads instead of toolArgs.
|
|
682
|
+
// ---------------------------------------------------------------------------
|
|
683
|
+
async presentCreate(args, ctx) {
|
|
684
|
+
const type = this.argType(args);
|
|
685
|
+
const entity = this.catalog.getEntityDetail(type, ctx.userModuleIds);
|
|
686
|
+
const scopeKey = entity ? (0, writable_rules_1.scopeKeyOf)(entity) : undefined;
|
|
687
|
+
const byName = new Map((entity?.relationships ?? []).map((relationship) => [relationship.name, relationship]));
|
|
688
|
+
const raw = args.relationships && typeof args.relationships === "object" ? args.relationships : {};
|
|
689
|
+
const relationships = {};
|
|
690
|
+
for (const [name, value] of Object.entries(raw)) {
|
|
691
|
+
// The run's own scope is not a user choice — it is never shown.
|
|
692
|
+
if (scopeKey && name === scopeKey)
|
|
693
|
+
continue;
|
|
694
|
+
const targetType = byName.get(name)?.targetType ?? name;
|
|
695
|
+
const refs = [];
|
|
696
|
+
for (const id of this.toIdList(value))
|
|
697
|
+
refs.push(await this.resolveRef({ type: targetType, id, ctx }));
|
|
698
|
+
if (refs.length)
|
|
699
|
+
relationships[name] = refs;
|
|
700
|
+
}
|
|
701
|
+
return {
|
|
702
|
+
type,
|
|
703
|
+
attributes: this.presentAttributes(args),
|
|
704
|
+
...(Object.keys(relationships).length ? { relationships } : {}),
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
async presentUpdate(args, ctx) {
|
|
708
|
+
return {
|
|
709
|
+
type: this.argType(args),
|
|
710
|
+
target: await this.resolveTargetRef(args, ctx),
|
|
711
|
+
attributes: this.presentAttributes(args),
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
async presentDelete(args, ctx) {
|
|
715
|
+
return {
|
|
716
|
+
type: this.argType(args),
|
|
717
|
+
target: await this.resolveTargetRef(args, ctx),
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
async presentLink(args, ctx) {
|
|
721
|
+
return {
|
|
722
|
+
type: this.argType(args),
|
|
723
|
+
target: await this.resolveTargetRef(args, ctx),
|
|
724
|
+
relationship: typeof args.relationship === "string" ? args.relationship : "",
|
|
725
|
+
targets: await this.resolveLinkTargets(args, ctx),
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
async resolveLinkTargets(args, ctx) {
|
|
729
|
+
const entity = this.catalog.getEntityDetail(this.argType(args), ctx.userModuleIds);
|
|
730
|
+
const name = typeof args.relationship === "string" ? args.relationship : "";
|
|
731
|
+
const targetType = entity?.relationships.find((relationship) => relationship.name === name)?.targetType ?? (name || "record");
|
|
732
|
+
const refs = [];
|
|
733
|
+
for (const id of this.toIdList(args.targetIds))
|
|
734
|
+
refs.push(await this.resolveRef({ type: targetType, id, ctx }));
|
|
735
|
+
return refs;
|
|
736
|
+
}
|
|
737
|
+
presentAttributes(args) {
|
|
738
|
+
return args.fields && typeof args.fields === "object" ? { ...args.fields } : {};
|
|
739
|
+
}
|
|
468
740
|
};
|
|
469
741
|
exports.EntityWriteTools = EntityWriteTools;
|
|
470
742
|
exports.EntityWriteTools = EntityWriteTools = EntityWriteTools_1 = __decorate([
|
|
@@ -472,6 +744,7 @@ exports.EntityWriteTools = EntityWriteTools = EntityWriteTools_1 = __decorate([
|
|
|
472
744
|
__metadata("design:paramtypes", [graph_catalog_service_1.GraphCatalogService,
|
|
473
745
|
entity_service_registry_1.EntityServiceRegistry,
|
|
474
746
|
scope_guard_1.ScopeGuard,
|
|
475
|
-
tool_factory_1.ToolFactory
|
|
747
|
+
tool_factory_1.ToolFactory,
|
|
748
|
+
blocknote_service_1.BlockNoteService])
|
|
476
749
|
], EntityWriteTools);
|
|
477
750
|
//# sourceMappingURL=entity-write.tools.js.map
|