@fabricorg/platform-host 0.1.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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +26 -0
- package/dist/index.cjs +698 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +187 -0
- package/dist/index.d.ts +187 -0
- package/dist/index.js +694 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var platform = require('@fabricorg/platform');
|
|
4
|
+
|
|
5
|
+
// src/host.ts
|
|
6
|
+
var DEFAULT_EXTRACT_EVENTS = (data) => {
|
|
7
|
+
const value = data._events;
|
|
8
|
+
return Array.isArray(value) ? value : [];
|
|
9
|
+
};
|
|
10
|
+
function createGovernedActionHost(options) {
|
|
11
|
+
const adapters = new platform.AdapterRegistry();
|
|
12
|
+
for (const adapter of options.adapters ?? []) adapters.register(adapter);
|
|
13
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
14
|
+
const extractEvents = options.extractEvents ?? DEFAULT_EXTRACT_EVENTS;
|
|
15
|
+
async function submitAction(input) {
|
|
16
|
+
const action = platform.resolveAction(input.actionId);
|
|
17
|
+
if (!action) throw new Error(`Unknown action: ${input.actionId}`);
|
|
18
|
+
const authorizationInput = toAuthorizationInput(action, input);
|
|
19
|
+
if (!await options.authorization.checkEntitlement(authorizationInput)) {
|
|
20
|
+
throw new Error(`Module "${action.namespace}" is not enabled for tenant ${input.tenantId}`);
|
|
21
|
+
}
|
|
22
|
+
if (!await options.authorization.authorize(authorizationInput)) {
|
|
23
|
+
throw new Error(`Actor ${input.actorId} is not authorized for action ${input.actionId}`);
|
|
24
|
+
}
|
|
25
|
+
const actionInvocationId = platform.createFabricId("act");
|
|
26
|
+
const correlationId = input.correlationId ?? platform.createFabricId("corr");
|
|
27
|
+
const workflowId = `action-invocation-${actionInvocationId}`;
|
|
28
|
+
await options.store.createActionInvocation({
|
|
29
|
+
id: actionInvocationId,
|
|
30
|
+
tenantId: input.tenantId,
|
|
31
|
+
spaceId: input.spaceId,
|
|
32
|
+
actionId: input.actionId,
|
|
33
|
+
actionVersion: action.version,
|
|
34
|
+
actorId: input.actorId,
|
|
35
|
+
actorType: input.actorType,
|
|
36
|
+
status: "pending",
|
|
37
|
+
parameters: input.parameters,
|
|
38
|
+
result: {},
|
|
39
|
+
correlationId,
|
|
40
|
+
...input.causationId ? { causationId: input.causationId } : {}
|
|
41
|
+
});
|
|
42
|
+
if (options.dispatcher) {
|
|
43
|
+
try {
|
|
44
|
+
const dispatched = await options.dispatcher.dispatch({
|
|
45
|
+
actionInvocationId,
|
|
46
|
+
tenantId: input.tenantId,
|
|
47
|
+
spaceId: input.spaceId,
|
|
48
|
+
workflowId
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
actionInvocationId,
|
|
52
|
+
status: "pending",
|
|
53
|
+
workflowId: dispatched.workflowId,
|
|
54
|
+
...dispatched.runId ? { runId: dispatched.runId } : {}
|
|
55
|
+
};
|
|
56
|
+
} catch (error) {
|
|
57
|
+
const message = errorMessage(error);
|
|
58
|
+
await options.store.updateActionInvocation(
|
|
59
|
+
actionInvocationId,
|
|
60
|
+
input.tenantId,
|
|
61
|
+
input.spaceId,
|
|
62
|
+
{ status: "failed", error: message }
|
|
63
|
+
);
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const executed = await executeInvocation(
|
|
68
|
+
actionInvocationId,
|
|
69
|
+
input.tenantId,
|
|
70
|
+
input.spaceId
|
|
71
|
+
);
|
|
72
|
+
return { ...executed, workflowId };
|
|
73
|
+
}
|
|
74
|
+
async function executeInvocation(actionInvocationId, tenantId, spaceId) {
|
|
75
|
+
const invocation = await options.store.getActionInvocation(
|
|
76
|
+
actionInvocationId,
|
|
77
|
+
tenantId,
|
|
78
|
+
spaceId
|
|
79
|
+
);
|
|
80
|
+
if (!invocation) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
|
|
81
|
+
if (isTerminal(invocation.status)) {
|
|
82
|
+
return {
|
|
83
|
+
actionInvocationId,
|
|
84
|
+
status: invocation.status,
|
|
85
|
+
result: invocation.result,
|
|
86
|
+
...invocation.error ? { error: invocation.error } : {}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const action = platform.resolveAction(invocation.actionId);
|
|
90
|
+
if (!action) {
|
|
91
|
+
return fail(invocation, "failed", `Unknown action: ${invocation.actionId}`);
|
|
92
|
+
}
|
|
93
|
+
await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, {
|
|
94
|
+
status: "running"
|
|
95
|
+
});
|
|
96
|
+
const parsed = action.schema.safeParse(invocation.parameters);
|
|
97
|
+
if (!parsed.success) {
|
|
98
|
+
const message = parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
99
|
+
return fail(invocation, "validation_failed", message);
|
|
100
|
+
}
|
|
101
|
+
const authorizationInput = toAuthorizationInput(action, invocation);
|
|
102
|
+
const definitions = options.resolvePolicies ? await options.resolvePolicies({
|
|
103
|
+
...authorizationInput,
|
|
104
|
+
declaredPolicyIds: action.policies ?? []
|
|
105
|
+
}) : declaredPolicies(action.policies ?? []);
|
|
106
|
+
const outcomes = await platform.evaluatePolicyDefinitions(definitions, {
|
|
107
|
+
tenantId,
|
|
108
|
+
spaceId,
|
|
109
|
+
actionInvocationId,
|
|
110
|
+
actionId: action.actionId,
|
|
111
|
+
actorId: invocation.actorId,
|
|
112
|
+
actorType: invocation.actorType,
|
|
113
|
+
parameters: parsed.data,
|
|
114
|
+
db: options.store.db,
|
|
115
|
+
services: options.services,
|
|
116
|
+
mode: "execute"
|
|
117
|
+
});
|
|
118
|
+
for (const outcome of outcomes) {
|
|
119
|
+
await options.store.appendPolicyEvaluation({
|
|
120
|
+
id: platform.createFabricId("pol"),
|
|
121
|
+
actionInvocationId,
|
|
122
|
+
tenantId,
|
|
123
|
+
spaceId,
|
|
124
|
+
outcome,
|
|
125
|
+
createdAt: now()
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const aggregate = platform.aggregatePolicyOutcomes(outcomes);
|
|
129
|
+
if (aggregate?.result === "block") {
|
|
130
|
+
const reason = aggregate.reason ?? `Blocked by policy ${aggregate.policyId}`;
|
|
131
|
+
await appendEvent(invocation, {
|
|
132
|
+
eventType: "ComplianceBlocked",
|
|
133
|
+
subjectType: "ActionInvocation",
|
|
134
|
+
subjectId: actionInvocationId,
|
|
135
|
+
payload: { actionId: action.actionId, policyId: aggregate.policyId, reason }
|
|
136
|
+
});
|
|
137
|
+
return fail(invocation, "blocked_by_policy", reason);
|
|
138
|
+
}
|
|
139
|
+
const binding = action.stateMachine;
|
|
140
|
+
if (binding) {
|
|
141
|
+
const entityId = binding.getEntityId(parsed.data);
|
|
142
|
+
const currentState = entityId ? await options.store.getEntityState(
|
|
143
|
+
tenantId,
|
|
144
|
+
spaceId,
|
|
145
|
+
binding.entityType,
|
|
146
|
+
entityId
|
|
147
|
+
) ?? initialState(binding.entityType) : initialState(binding.entityType);
|
|
148
|
+
const targetState = typeof binding.targetState === "function" ? binding.targetState(parsed.data) : binding.targetState;
|
|
149
|
+
const transition = platform.validateTransition(
|
|
150
|
+
binding.entityType,
|
|
151
|
+
currentState,
|
|
152
|
+
targetState,
|
|
153
|
+
action.actionId
|
|
154
|
+
);
|
|
155
|
+
if (!transition.valid) {
|
|
156
|
+
return fail(invocation, "failed", transition.error ?? "Invalid state transition");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
let data;
|
|
160
|
+
let domainEvents = [];
|
|
161
|
+
try {
|
|
162
|
+
const handlerResult = await options.store.transaction(
|
|
163
|
+
(db) => action.handler ? action.handler(
|
|
164
|
+
{
|
|
165
|
+
actionInvocationId,
|
|
166
|
+
tenantId,
|
|
167
|
+
spaceId,
|
|
168
|
+
actorId: invocation.actorId,
|
|
169
|
+
actorType: invocation.actorType,
|
|
170
|
+
correlationId: invocation.correlationId,
|
|
171
|
+
...invocation.causationId ? { causationId: invocation.causationId } : {},
|
|
172
|
+
db,
|
|
173
|
+
services: options.services
|
|
174
|
+
},
|
|
175
|
+
parsed.data
|
|
176
|
+
) : Promise.resolve({ success: true, data: {} })
|
|
177
|
+
);
|
|
178
|
+
if (!handlerResult.success) {
|
|
179
|
+
return fail(invocation, "failed", handlerResult.error ?? "Action handler failed");
|
|
180
|
+
}
|
|
181
|
+
data = handlerResult.data ?? {};
|
|
182
|
+
domainEvents = extractEvents(data);
|
|
183
|
+
if (action.eventPhase !== "after_adapters") {
|
|
184
|
+
for (const event of domainEvents) await appendEvent(invocation, event);
|
|
185
|
+
}
|
|
186
|
+
} catch (error) {
|
|
187
|
+
return fail(invocation, "failed", errorMessage(error));
|
|
188
|
+
}
|
|
189
|
+
for (const step of action.adapterSteps ?? []) {
|
|
190
|
+
const input = step.getInput(parsed.data, data);
|
|
191
|
+
if (!input) continue;
|
|
192
|
+
const adapter = adapters.require(step.adapterType, step.operation);
|
|
193
|
+
const adapterInvocationId = platform.createFabricId("adp");
|
|
194
|
+
const adapterEventSubject = options.adapterEventSubject?.(
|
|
195
|
+
action.actionId,
|
|
196
|
+
parsed.data,
|
|
197
|
+
data
|
|
198
|
+
) ?? { subjectType: "AdapterInvocation", subjectId: adapterInvocationId };
|
|
199
|
+
const recordedInput = options.redactAdapterInput ? options.redactAdapterInput(
|
|
200
|
+
action.actionId,
|
|
201
|
+
step.adapterType,
|
|
202
|
+
step.operation,
|
|
203
|
+
input
|
|
204
|
+
) : input;
|
|
205
|
+
const startedAt = now();
|
|
206
|
+
const record = {
|
|
207
|
+
id: adapterInvocationId,
|
|
208
|
+
actionInvocationId,
|
|
209
|
+
tenantId,
|
|
210
|
+
spaceId,
|
|
211
|
+
adapterType: step.adapterType,
|
|
212
|
+
operation: step.operation,
|
|
213
|
+
vendor: adapter.vendor,
|
|
214
|
+
status: "running",
|
|
215
|
+
input: recordedInput,
|
|
216
|
+
attempt: 1,
|
|
217
|
+
createdAt: startedAt,
|
|
218
|
+
updatedAt: startedAt
|
|
219
|
+
};
|
|
220
|
+
await options.store.createAdapterInvocation(record);
|
|
221
|
+
await appendEvent(invocation, {
|
|
222
|
+
eventType: "AdapterInvocationStarted",
|
|
223
|
+
subjectType: adapterEventSubject.subjectType,
|
|
224
|
+
subjectId: adapterEventSubject.subjectId,
|
|
225
|
+
payload: { adapterType: step.adapterType, operation: step.operation }
|
|
226
|
+
});
|
|
227
|
+
try {
|
|
228
|
+
const result2 = await platform.executeWithAdapterRetry({
|
|
229
|
+
policy: step.retryPolicy ?? adapter.retryPolicy,
|
|
230
|
+
defaultIdempotent: adapter.idempotent,
|
|
231
|
+
execute: async (attempt, maxAttempts) => {
|
|
232
|
+
await options.store.updateAdapterInvocation(adapterInvocationId, {
|
|
233
|
+
attempt,
|
|
234
|
+
updatedAt: now()
|
|
235
|
+
});
|
|
236
|
+
return adapter.execute(input, {
|
|
237
|
+
tenantId,
|
|
238
|
+
spaceId,
|
|
239
|
+
actionInvocationId,
|
|
240
|
+
adapterInvocationId,
|
|
241
|
+
correlationId: invocation.correlationId,
|
|
242
|
+
...invocation.causationId ? { causationId: invocation.causationId } : {},
|
|
243
|
+
attempt,
|
|
244
|
+
maxAttempts
|
|
245
|
+
});
|
|
246
|
+
},
|
|
247
|
+
isSuccessful: (result3) => result3.success,
|
|
248
|
+
getError: (result3) => result3.error
|
|
249
|
+
});
|
|
250
|
+
if (!result2.success) {
|
|
251
|
+
await options.store.updateAdapterInvocation(adapterInvocationId, {
|
|
252
|
+
status: "failed",
|
|
253
|
+
error: result2.error ?? "Adapter failed",
|
|
254
|
+
updatedAt: now()
|
|
255
|
+
});
|
|
256
|
+
await appendEvent(invocation, {
|
|
257
|
+
eventType: "AdapterInvocationFailed",
|
|
258
|
+
subjectType: adapterEventSubject.subjectType,
|
|
259
|
+
subjectId: adapterEventSubject.subjectId,
|
|
260
|
+
payload: { adapterType: step.adapterType, operation: step.operation }
|
|
261
|
+
});
|
|
262
|
+
return fail(invocation, "failed", result2.error ?? "Adapter failed");
|
|
263
|
+
}
|
|
264
|
+
const resultRecord = result2;
|
|
265
|
+
const output = isRecord(resultRecord.output) ? resultRecord.output : Object.fromEntries(
|
|
266
|
+
Object.entries(resultRecord).filter(
|
|
267
|
+
([key]) => key !== "success" && key !== "error"
|
|
268
|
+
)
|
|
269
|
+
);
|
|
270
|
+
await options.store.updateAdapterInvocation(adapterInvocationId, {
|
|
271
|
+
status: "succeeded",
|
|
272
|
+
output,
|
|
273
|
+
updatedAt: now()
|
|
274
|
+
});
|
|
275
|
+
await appendEvent(invocation, {
|
|
276
|
+
eventType: "AdapterInvocationSucceeded",
|
|
277
|
+
subjectType: adapterEventSubject.subjectType,
|
|
278
|
+
subjectId: adapterEventSubject.subjectId,
|
|
279
|
+
payload: {
|
|
280
|
+
adapterType: step.adapterType,
|
|
281
|
+
operation: step.operation,
|
|
282
|
+
input: recordedInput,
|
|
283
|
+
output
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
} catch (error) {
|
|
287
|
+
const message = errorMessage(error);
|
|
288
|
+
await options.store.updateAdapterInvocation(adapterInvocationId, {
|
|
289
|
+
status: "failed",
|
|
290
|
+
error: message,
|
|
291
|
+
updatedAt: now()
|
|
292
|
+
});
|
|
293
|
+
return fail(invocation, "failed", message);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (action.eventPhase === "after_adapters") {
|
|
297
|
+
for (const event of domainEvents) await appendEvent(invocation, event);
|
|
298
|
+
}
|
|
299
|
+
const result = withoutPrivateHostFields(data);
|
|
300
|
+
await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, {
|
|
301
|
+
status: "completed",
|
|
302
|
+
result
|
|
303
|
+
});
|
|
304
|
+
return { actionInvocationId, status: "completed", result };
|
|
305
|
+
}
|
|
306
|
+
async function appendEvent(invocation, event) {
|
|
307
|
+
const timestamp = now();
|
|
308
|
+
const envelope = {
|
|
309
|
+
id: platform.createFabricId("evt"),
|
|
310
|
+
tenantId: invocation.tenantId,
|
|
311
|
+
spaceId: invocation.spaceId,
|
|
312
|
+
eventType: event.eventType,
|
|
313
|
+
eventSchemaVersion: event.eventSchemaVersion ?? 1,
|
|
314
|
+
subjectType: event.subjectType,
|
|
315
|
+
subjectId: event.subjectId,
|
|
316
|
+
actorId: invocation.actorId,
|
|
317
|
+
actorType: invocation.actorType,
|
|
318
|
+
actionInvocationId: invocation.id,
|
|
319
|
+
payload: event.payload,
|
|
320
|
+
sequence: await options.store.nextEventSequence(
|
|
321
|
+
invocation.tenantId,
|
|
322
|
+
invocation.spaceId
|
|
323
|
+
),
|
|
324
|
+
occurredAt: timestamp,
|
|
325
|
+
recordedAt: timestamp,
|
|
326
|
+
correlationId: invocation.correlationId,
|
|
327
|
+
...invocation.causationId ? { causationId: invocation.causationId } : {}
|
|
328
|
+
};
|
|
329
|
+
await options.store.appendEvent(envelope);
|
|
330
|
+
}
|
|
331
|
+
async function fail(invocation, status, error) {
|
|
332
|
+
await options.store.updateActionInvocation(
|
|
333
|
+
invocation.id,
|
|
334
|
+
invocation.tenantId,
|
|
335
|
+
invocation.spaceId,
|
|
336
|
+
{ status, error }
|
|
337
|
+
);
|
|
338
|
+
return { actionInvocationId: invocation.id, status, error };
|
|
339
|
+
}
|
|
340
|
+
return { submitAction, executeInvocation };
|
|
341
|
+
}
|
|
342
|
+
function toAuthorizationInput(action, input) {
|
|
343
|
+
return {
|
|
344
|
+
actionId: action.actionId,
|
|
345
|
+
namespace: action.namespace,
|
|
346
|
+
requiredPermissions: action.requiredPermissions ?? [],
|
|
347
|
+
requiredRoles: action.requiredRoles ?? [],
|
|
348
|
+
tenantId: input.tenantId,
|
|
349
|
+
spaceId: input.spaceId,
|
|
350
|
+
actorId: input.actorId,
|
|
351
|
+
actorType: input.actorType
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function declaredPolicies(policyIds) {
|
|
355
|
+
return policyIds.map((policyId) => ({
|
|
356
|
+
policyId,
|
|
357
|
+
policyVersion: Number(policyId.split(".v").at(-1) ?? 1),
|
|
358
|
+
kind: "code",
|
|
359
|
+
codeEvaluatorPolicyId: policyId
|
|
360
|
+
}));
|
|
361
|
+
}
|
|
362
|
+
function initialState(entityType) {
|
|
363
|
+
const machine = platform.resolveStateMachine(entityType);
|
|
364
|
+
return Object.values(machine?.states ?? {}).find((state) => state.stateClass === "initial")?.id ?? "none";
|
|
365
|
+
}
|
|
366
|
+
function isTerminal(status) {
|
|
367
|
+
return ["completed", "failed", "blocked_by_policy", "validation_failed"].includes(status);
|
|
368
|
+
}
|
|
369
|
+
function withoutPrivateHostFields(data) {
|
|
370
|
+
const { _events: _ignored, ...result } = data;
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
function errorMessage(error) {
|
|
374
|
+
return error instanceof Error ? error.message : String(error);
|
|
375
|
+
}
|
|
376
|
+
function isRecord(value) {
|
|
377
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/memory-store.ts
|
|
381
|
+
var MemoryPlatformHostStore = class {
|
|
382
|
+
constructor(db) {
|
|
383
|
+
this.db = db;
|
|
384
|
+
}
|
|
385
|
+
db;
|
|
386
|
+
invocations = [];
|
|
387
|
+
policyEvaluations = [];
|
|
388
|
+
adapterInvocations = [];
|
|
389
|
+
events = [];
|
|
390
|
+
async transaction(run) {
|
|
391
|
+
return run(this.db);
|
|
392
|
+
}
|
|
393
|
+
async createActionInvocation(input) {
|
|
394
|
+
const now = /* @__PURE__ */ new Date();
|
|
395
|
+
const record = { ...input, createdAt: now, updatedAt: now };
|
|
396
|
+
this.invocations.push(record);
|
|
397
|
+
return record;
|
|
398
|
+
}
|
|
399
|
+
async getActionInvocation(id, tenantId, spaceId) {
|
|
400
|
+
return this.invocations.find(
|
|
401
|
+
(record) => record.id === id && record.tenantId === tenantId && record.spaceId === spaceId
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
async updateActionInvocation(id, tenantId, spaceId, patch) {
|
|
405
|
+
const record = await this.getActionInvocation(id, tenantId, spaceId);
|
|
406
|
+
if (!record) throw new Error(`ActionInvocation not found: ${id}`);
|
|
407
|
+
Object.assign(record, patch, { updatedAt: /* @__PURE__ */ new Date() });
|
|
408
|
+
}
|
|
409
|
+
async appendPolicyEvaluation(record) {
|
|
410
|
+
this.policyEvaluations.push(record);
|
|
411
|
+
}
|
|
412
|
+
async createAdapterInvocation(record) {
|
|
413
|
+
this.adapterInvocations.push(record);
|
|
414
|
+
}
|
|
415
|
+
async updateAdapterInvocation(id, patch) {
|
|
416
|
+
const record = this.adapterInvocations.find((candidate) => candidate.id === id);
|
|
417
|
+
if (!record) throw new Error(`AdapterInvocation not found: ${id}`);
|
|
418
|
+
Object.assign(record, patch);
|
|
419
|
+
}
|
|
420
|
+
async appendEvent(event) {
|
|
421
|
+
this.events.push(event);
|
|
422
|
+
}
|
|
423
|
+
async nextEventSequence(tenantId, spaceId) {
|
|
424
|
+
return this.events.filter((event) => event.tenantId === tenantId && event.spaceId === spaceId).length + 1;
|
|
425
|
+
}
|
|
426
|
+
async getEntityState(tenantId, spaceId, entityType, entityId) {
|
|
427
|
+
let state;
|
|
428
|
+
for (const event of this.events) {
|
|
429
|
+
if (event.tenantId !== tenantId || event.spaceId !== spaceId || event.subjectType !== entityType || event.subjectId !== entityId) {
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
const candidate = event.payload?.toState;
|
|
433
|
+
if (typeof candidate === "string") state = candidate;
|
|
434
|
+
}
|
|
435
|
+
return state;
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// src/postgres-store.ts
|
|
440
|
+
var PostgresPlatformHostStore = class {
|
|
441
|
+
constructor(db, sql) {
|
|
442
|
+
this.db = db;
|
|
443
|
+
this.sql = sql;
|
|
444
|
+
}
|
|
445
|
+
db;
|
|
446
|
+
sql;
|
|
447
|
+
async ensureSchema() {
|
|
448
|
+
await this.sql.query(`
|
|
449
|
+
CREATE SCHEMA IF NOT EXISTS fabric_platform;
|
|
450
|
+
CREATE TABLE IF NOT EXISTS fabric_platform.action_invocations (
|
|
451
|
+
id text PRIMARY KEY, tenant_id text NOT NULL, space_id text NOT NULL,
|
|
452
|
+
action_id text NOT NULL, action_version integer NOT NULL,
|
|
453
|
+
actor_id text NOT NULL, actor_type text NOT NULL, status text NOT NULL,
|
|
454
|
+
parameters jsonb NOT NULL, result jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
455
|
+
correlation_id text NOT NULL, causation_id text, error text,
|
|
456
|
+
created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL
|
|
457
|
+
);
|
|
458
|
+
CREATE TABLE IF NOT EXISTS fabric_platform.policy_evaluations (
|
|
459
|
+
id text PRIMARY KEY, action_invocation_id text NOT NULL,
|
|
460
|
+
tenant_id text NOT NULL, space_id text NOT NULL, outcome jsonb NOT NULL,
|
|
461
|
+
created_at timestamptz NOT NULL
|
|
462
|
+
);
|
|
463
|
+
CREATE TABLE IF NOT EXISTS fabric_platform.adapter_invocations (
|
|
464
|
+
id text PRIMARY KEY, action_invocation_id text NOT NULL,
|
|
465
|
+
tenant_id text NOT NULL, space_id text NOT NULL, adapter_type text NOT NULL,
|
|
466
|
+
operation text NOT NULL, vendor text NOT NULL, status text NOT NULL,
|
|
467
|
+
input jsonb NOT NULL, output jsonb, error text, attempt integer NOT NULL,
|
|
468
|
+
created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL
|
|
469
|
+
);
|
|
470
|
+
CREATE TABLE IF NOT EXISTS fabric_platform.event_sequences (
|
|
471
|
+
tenant_id text NOT NULL, space_id text NOT NULL, next_sequence bigint NOT NULL,
|
|
472
|
+
PRIMARY KEY (tenant_id, space_id)
|
|
473
|
+
);
|
|
474
|
+
CREATE TABLE IF NOT EXISTS fabric_platform.asset_events (
|
|
475
|
+
id text PRIMARY KEY, tenant_id text NOT NULL, space_id text NOT NULL,
|
|
476
|
+
event_type text NOT NULL, event_schema_version integer NOT NULL,
|
|
477
|
+
subject_type text NOT NULL, subject_id text NOT NULL,
|
|
478
|
+
actor_id text NOT NULL, actor_type text NOT NULL, action_invocation_id text,
|
|
479
|
+
payload jsonb NOT NULL, sequence bigint NOT NULL,
|
|
480
|
+
occurred_at timestamptz NOT NULL, recorded_at timestamptz NOT NULL,
|
|
481
|
+
correlation_id text NOT NULL, causation_id text,
|
|
482
|
+
UNIQUE (tenant_id, space_id, sequence)
|
|
483
|
+
);
|
|
484
|
+
CREATE INDEX IF NOT EXISTS asset_events_subject_idx
|
|
485
|
+
ON fabric_platform.asset_events
|
|
486
|
+
(tenant_id, space_id, subject_type, subject_id, sequence);
|
|
487
|
+
`);
|
|
488
|
+
}
|
|
489
|
+
async transaction(run) {
|
|
490
|
+
return run(this.db);
|
|
491
|
+
}
|
|
492
|
+
async createActionInvocation(input) {
|
|
493
|
+
const now = /* @__PURE__ */ new Date();
|
|
494
|
+
await this.sql.query(
|
|
495
|
+
`INSERT INTO fabric_platform.action_invocations
|
|
496
|
+
(id,tenant_id,space_id,action_id,action_version,actor_id,actor_type,status,
|
|
497
|
+
parameters,result,correlation_id,causation_id,error,created_at,updated_at)
|
|
498
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb,$10::jsonb,$11,$12,$13,$14,$14)`,
|
|
499
|
+
[
|
|
500
|
+
input.id,
|
|
501
|
+
input.tenantId,
|
|
502
|
+
input.spaceId,
|
|
503
|
+
input.actionId,
|
|
504
|
+
input.actionVersion,
|
|
505
|
+
input.actorId,
|
|
506
|
+
input.actorType,
|
|
507
|
+
input.status,
|
|
508
|
+
JSON.stringify(input.parameters),
|
|
509
|
+
JSON.stringify(input.result),
|
|
510
|
+
input.correlationId,
|
|
511
|
+
input.causationId ?? null,
|
|
512
|
+
input.error ?? null,
|
|
513
|
+
now
|
|
514
|
+
]
|
|
515
|
+
);
|
|
516
|
+
return { ...input, createdAt: now, updatedAt: now };
|
|
517
|
+
}
|
|
518
|
+
async getActionInvocation(id, tenantId, spaceId) {
|
|
519
|
+
const result = await this.sql.query(
|
|
520
|
+
`SELECT * FROM fabric_platform.action_invocations
|
|
521
|
+
WHERE id=$1 AND tenant_id=$2 AND space_id=$3`,
|
|
522
|
+
[id, tenantId, spaceId]
|
|
523
|
+
);
|
|
524
|
+
return result.rows[0] ? toActionRecord(result.rows[0]) : void 0;
|
|
525
|
+
}
|
|
526
|
+
async updateActionInvocation(id, tenantId, spaceId, patch) {
|
|
527
|
+
await this.sql.query(
|
|
528
|
+
`UPDATE fabric_platform.action_invocations SET
|
|
529
|
+
status=COALESCE($4,status), result=COALESCE($5::jsonb,result),
|
|
530
|
+
error=CASE WHEN $6::boolean THEN $7 ELSE error END, updated_at=now()
|
|
531
|
+
WHERE id=$1 AND tenant_id=$2 AND space_id=$3`,
|
|
532
|
+
[
|
|
533
|
+
id,
|
|
534
|
+
tenantId,
|
|
535
|
+
spaceId,
|
|
536
|
+
patch.status ?? null,
|
|
537
|
+
patch.result === void 0 ? null : JSON.stringify(patch.result),
|
|
538
|
+
Object.hasOwn(patch, "error"),
|
|
539
|
+
patch.error ?? null
|
|
540
|
+
]
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
async appendPolicyEvaluation(record) {
|
|
544
|
+
await this.sql.query(
|
|
545
|
+
`INSERT INTO fabric_platform.policy_evaluations
|
|
546
|
+
(id,action_invocation_id,tenant_id,space_id,outcome,created_at)
|
|
547
|
+
VALUES ($1,$2,$3,$4,$5::jsonb,$6)`,
|
|
548
|
+
[
|
|
549
|
+
record.id,
|
|
550
|
+
record.actionInvocationId,
|
|
551
|
+
record.tenantId,
|
|
552
|
+
record.spaceId,
|
|
553
|
+
JSON.stringify(record.outcome),
|
|
554
|
+
record.createdAt
|
|
555
|
+
]
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
async createAdapterInvocation(record) {
|
|
559
|
+
await this.sql.query(
|
|
560
|
+
`INSERT INTO fabric_platform.adapter_invocations
|
|
561
|
+
(id,action_invocation_id,tenant_id,space_id,adapter_type,operation,vendor,status,
|
|
562
|
+
input,output,error,attempt,created_at,updated_at)
|
|
563
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb,$10::jsonb,$11,$12,$13,$14)`,
|
|
564
|
+
[
|
|
565
|
+
record.id,
|
|
566
|
+
record.actionInvocationId,
|
|
567
|
+
record.tenantId,
|
|
568
|
+
record.spaceId,
|
|
569
|
+
record.adapterType,
|
|
570
|
+
record.operation,
|
|
571
|
+
record.vendor,
|
|
572
|
+
record.status,
|
|
573
|
+
JSON.stringify(record.input),
|
|
574
|
+
record.output ? JSON.stringify(record.output) : null,
|
|
575
|
+
record.error ?? null,
|
|
576
|
+
record.attempt,
|
|
577
|
+
record.createdAt,
|
|
578
|
+
record.updatedAt
|
|
579
|
+
]
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
async updateAdapterInvocation(id, patch) {
|
|
583
|
+
await this.sql.query(
|
|
584
|
+
`UPDATE fabric_platform.adapter_invocations SET
|
|
585
|
+
status=COALESCE($2,status), output=COALESCE($3::jsonb,output),
|
|
586
|
+
error=CASE WHEN $4::boolean THEN $5 ELSE error END,
|
|
587
|
+
attempt=COALESCE($6,attempt), updated_at=COALESCE($7,now()) WHERE id=$1`,
|
|
588
|
+
[
|
|
589
|
+
id,
|
|
590
|
+
patch.status ?? null,
|
|
591
|
+
patch.output === void 0 ? null : JSON.stringify(patch.output),
|
|
592
|
+
Object.hasOwn(patch, "error"),
|
|
593
|
+
patch.error ?? null,
|
|
594
|
+
patch.attempt ?? null,
|
|
595
|
+
patch.updatedAt ?? null
|
|
596
|
+
]
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
async appendEvent(event) {
|
|
600
|
+
await this.sql.query(
|
|
601
|
+
`INSERT INTO fabric_platform.asset_events
|
|
602
|
+
(id,tenant_id,space_id,event_type,event_schema_version,subject_type,subject_id,
|
|
603
|
+
actor_id,actor_type,action_invocation_id,payload,sequence,occurred_at,recorded_at,
|
|
604
|
+
correlation_id,causation_id)
|
|
605
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13,$14,$15,$16)`,
|
|
606
|
+
[
|
|
607
|
+
event.id,
|
|
608
|
+
event.tenantId,
|
|
609
|
+
event.spaceId,
|
|
610
|
+
event.eventType,
|
|
611
|
+
event.eventSchemaVersion,
|
|
612
|
+
event.subjectType,
|
|
613
|
+
event.subjectId,
|
|
614
|
+
event.actorId,
|
|
615
|
+
event.actorType,
|
|
616
|
+
event.actionInvocationId ?? null,
|
|
617
|
+
JSON.stringify(event.payload),
|
|
618
|
+
event.sequence,
|
|
619
|
+
event.occurredAt,
|
|
620
|
+
event.recordedAt,
|
|
621
|
+
event.correlationId,
|
|
622
|
+
event.causationId ?? null
|
|
623
|
+
]
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
async nextEventSequence(tenantId, spaceId) {
|
|
627
|
+
const result = await this.sql.query(
|
|
628
|
+
`INSERT INTO fabric_platform.event_sequences (tenant_id,space_id,next_sequence)
|
|
629
|
+
VALUES ($1,$2,1) ON CONFLICT (tenant_id,space_id) DO UPDATE
|
|
630
|
+
SET next_sequence=fabric_platform.event_sequences.next_sequence+1
|
|
631
|
+
RETURNING next_sequence`,
|
|
632
|
+
[tenantId, spaceId]
|
|
633
|
+
);
|
|
634
|
+
return Number(result.rows[0].next_sequence);
|
|
635
|
+
}
|
|
636
|
+
async getEntityState(tenantId, spaceId, entityType, entityId) {
|
|
637
|
+
const result = await this.sql.query(
|
|
638
|
+
`SELECT payload->>'toState' AS state FROM fabric_platform.asset_events
|
|
639
|
+
WHERE tenant_id=$1 AND space_id=$2 AND subject_type=$3 AND subject_id=$4
|
|
640
|
+
AND payload ? 'toState' ORDER BY sequence DESC LIMIT 1`,
|
|
641
|
+
[tenantId, spaceId, entityType, entityId]
|
|
642
|
+
);
|
|
643
|
+
return result.rows[0]?.state;
|
|
644
|
+
}
|
|
645
|
+
async listEvents(tenantId, spaceId) {
|
|
646
|
+
const result = await this.sql.query(
|
|
647
|
+
`SELECT * FROM fabric_platform.asset_events
|
|
648
|
+
WHERE tenant_id=$1 AND space_id=$2 ORDER BY sequence`,
|
|
649
|
+
[tenantId, spaceId]
|
|
650
|
+
);
|
|
651
|
+
return result.rows.map(toEventRecord);
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
function toActionRecord(row) {
|
|
655
|
+
return {
|
|
656
|
+
id: String(row.id),
|
|
657
|
+
tenantId: String(row.tenant_id),
|
|
658
|
+
spaceId: String(row.space_id),
|
|
659
|
+
actionId: String(row.action_id),
|
|
660
|
+
actionVersion: Number(row.action_version),
|
|
661
|
+
actorId: String(row.actor_id),
|
|
662
|
+
actorType: String(row.actor_type),
|
|
663
|
+
status: String(row.status),
|
|
664
|
+
parameters: row.parameters,
|
|
665
|
+
result: row.result,
|
|
666
|
+
correlationId: String(row.correlation_id),
|
|
667
|
+
...row.causation_id ? { causationId: String(row.causation_id) } : {},
|
|
668
|
+
...row.error ? { error: String(row.error) } : {},
|
|
669
|
+
createdAt: new Date(row.created_at),
|
|
670
|
+
updatedAt: new Date(row.updated_at)
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
function toEventRecord(row) {
|
|
674
|
+
return {
|
|
675
|
+
id: String(row.id),
|
|
676
|
+
tenantId: String(row.tenant_id),
|
|
677
|
+
spaceId: String(row.space_id),
|
|
678
|
+
eventType: String(row.event_type),
|
|
679
|
+
eventSchemaVersion: Number(row.event_schema_version),
|
|
680
|
+
subjectType: String(row.subject_type),
|
|
681
|
+
subjectId: String(row.subject_id),
|
|
682
|
+
actorId: String(row.actor_id),
|
|
683
|
+
actorType: String(row.actor_type),
|
|
684
|
+
...row.action_invocation_id ? { actionInvocationId: String(row.action_invocation_id) } : {},
|
|
685
|
+
payload: row.payload,
|
|
686
|
+
sequence: Number(row.sequence),
|
|
687
|
+
occurredAt: new Date(row.occurred_at),
|
|
688
|
+
recordedAt: new Date(row.recorded_at),
|
|
689
|
+
correlationId: String(row.correlation_id),
|
|
690
|
+
...row.causation_id ? { causationId: String(row.causation_id) } : {}
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
exports.MemoryPlatformHostStore = MemoryPlatformHostStore;
|
|
695
|
+
exports.PostgresPlatformHostStore = PostgresPlatformHostStore;
|
|
696
|
+
exports.createGovernedActionHost = createGovernedActionHost;
|
|
697
|
+
//# sourceMappingURL=index.cjs.map
|
|
698
|
+
//# sourceMappingURL=index.cjs.map
|