@flowdular/sdk 0.3.1 → 0.3.2
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/.ai/platform-capabilities.md +2 -0
- package/.ai/references/catalog/migrations/0005_catalog_list_indexes.down.sql +3 -0
- package/.ai/references/catalog/migrations/0005_catalog_list_indexes.up.sql +11 -0
- package/.ai/references/catalog/module.json +11 -1
- package/.ai/references/catalog/package.json +2 -2
- package/.ai/references/catalog/spec/module.yaml +25 -4
- package/.ai/references/catalog/src/agent/tools.ts +19 -10
- package/.ai/references/catalog/src/api/endpoints.ts +150 -10
- package/.ai/references/catalog/src/api/list-cursor.ts +83 -0
- package/.ai/references/catalog/src/client/CatalogView.tsrx +505 -159
- package/.ai/references/catalog/src/client/api.ts +124 -36
- package/.ai/references/catalog/src/client/contribution.tsrx +5 -0
- package/.ai/references/catalog/src/client/state.ts +169 -3
- package/.ai/references/catalog/src/domain/lists.ts +7 -0
- package/.ai/references/catalog/src/domain/types.ts +20 -0
- package/.ai/references/catalog/src/platform.ts +20 -0
- package/.ai/references/catalog/src/services/catalog-service.ts +143 -8
- package/.ai/references/catalog/src/services/database-repository.ts +104 -17
- package/.ai/references/catalog/src/services/item-export.ts +81 -0
- package/.ai/references/catalog/src/services/migration.ts +27 -1
- package/.ai/references/catalog/src/services/repository.ts +31 -2
- package/.ai/references/catalog/tests/agent-tools.test.ts +6 -5
- package/.ai/references/catalog/tests/client-state.test.ts +124 -0
- package/.ai/references/catalog/tests/endpoints.test.ts +269 -0
- package/.ai/references/catalog/tests/export.test.ts +134 -0
- package/.ai/references/catalog/tests/idempotency.test.ts +15 -14
- package/.ai/references/catalog/tests/list.test.ts +217 -0
- package/.ai/references/catalog/tests/migrations.test.ts +58 -2
- package/.ai/references/catalog/tests/module.test.ts +2 -1
- package/.ai/references/catalog/tests/support/database.ts +14 -0
- package/.ai/references/catalog/translations/en.json +35 -4
- package/.ai/references/catalog/translations/pl.json +35 -4
- package/.ai/references/catalog.provenance.json +34 -26
- package/README.md +1 -1
- package/assets/flowdular-banner.webp +0 -0
- package/modules/access/module.json +1 -1
- package/modules/access/spec/module.yaml +1 -1
- package/modules/agents/module.json +1 -1
- package/modules/agents/spec/module.yaml +1 -1
- package/modules/approvals/module.json +1 -1
- package/modules/approvals/spec/module.yaml +1 -1
- package/modules/audit/module.json +1 -1
- package/modules/audit/spec/module.yaml +1 -1
- package/modules/auth/module.json +2 -2
- package/modules/auth/package.json +1 -1
- package/modules/auth/spec/module.yaml +2 -2
- package/modules/auth/src/middleware/authentication.ts +5 -1
- package/modules/auth/src/services/auth-service.ts +26 -0
- package/modules/auth/src/services/database-repository.ts +9 -0
- package/modules/auth/src/services/repository.ts +2 -0
- package/modules/automations/module.json +1 -1
- package/modules/automations/spec/module.yaml +1 -1
- package/modules/connectors/module.json +1 -1
- package/modules/connectors/spec/module.yaml +1 -1
- package/modules/directory/module.json +1 -1
- package/modules/directory/spec/module.yaml +1 -1
- package/modules/documents/module.json +1 -1
- package/modules/documents/spec/module.yaml +1 -1
- package/modules/exports/module.json +1 -1
- package/modules/exports/spec/module.yaml +1 -1
- package/modules/import/module.json +1 -1
- package/modules/import/spec/module.yaml +1 -1
- package/modules/metering/module.json +1 -1
- package/modules/metering/spec/module.yaml +1 -1
- package/modules/notifications/module.json +1 -1
- package/modules/notifications/spec/module.yaml +1 -1
- package/modules/reports/module.json +1 -1
- package/modules/reports/spec/module.yaml +1 -1
- package/modules/sandbox/module.json +1 -1
- package/modules/sandbox/spec/module.yaml +1 -1
- package/modules/search/module.json +1 -1
- package/modules/search/spec/module.yaml +1 -1
- package/modules/system/migrations/0001_system_module_activations.down.sql +2 -0
- package/modules/system/migrations/0001_system_module_activations.up.sql +17 -0
- package/modules/system/module.json +3 -2
- package/modules/system/package.json +2 -2
- package/modules/system/spec/module.yaml +98 -1
- package/modules/system/src/client/ModulesView.tsrx +101 -8
- package/modules/system/src/client/api.ts +52 -11
- package/modules/system/src/client/module-columns.tsrx +32 -6
- package/modules/system/src/client/state.ts +26 -0
- package/modules/system/src/domain/modules.ts +93 -0
- package/modules/system/src/index.ts +13 -0
- package/modules/system/src/platform.ts +41 -1
- package/modules/system/src/server/capability.ts +6 -0
- package/modules/system/src/server/endpoints.ts +125 -5
- package/modules/system/src/server/index.ts +26 -0
- package/modules/system/src/server/module-catalog.ts +19 -0
- package/modules/system/src/server/runtime.ts +102 -0
- package/modules/system/src/services/database-repository.ts +91 -0
- package/modules/system/src/services/migration.ts +39 -0
- package/modules/system/src/services/module-activation-service.ts +245 -0
- package/modules/system/src/services/repository.ts +17 -0
- package/modules/system/translations/en.json +17 -1
- package/modules/system/translations/pl.json +17 -1
- package/modules/users/module.json +2 -2
- package/modules/users/package.json +1 -1
- package/modules/users/spec/module.yaml +6 -9
- package/modules/users/src/client/UsersView.tsrx +27 -13
- package/modules/users/src/client/member-columns.tsrx +13 -4
- package/modules/users/src/client/state.ts +16 -0
- package/modules/users/src/services/users-service.ts +3 -0
- package/modules/users/translations/en.json +2 -1
- package/modules/users/translations/pl.json +2 -1
- package/modules/workflows/module.json +2 -2
- package/modules/workflows/package.json +1 -1
- package/modules/workflows/spec/module.yaml +2 -2
- package/modules/workflows/src/services/database-repository.ts +93 -53
- package/package.json +1 -1
- package/packages/client/package.json +1 -0
- package/packages/client/src/ApplicationShell.tsrx +46 -5
- package/packages/client/src/index.ts +5 -0
- package/packages/client/src/shell/modules.ts +49 -0
- package/packages/client/src/shell/types.ts +3 -0
- package/packages/contracts/src/index.ts +11 -1
- package/packages/server/src/endpoint.ts +17 -1
- package/packages/server/src/index.ts +10 -0
- package/packages/server/src/module-activation.ts +69 -0
- package/assets/flowdular-banner.png +0 -0
|
@@ -11,14 +11,14 @@ export function memberColumns(
|
|
|
11
11
|
{
|
|
12
12
|
key: 'displayName',
|
|
13
13
|
header: t('users.table.user'),
|
|
14
|
-
width: '
|
|
14
|
+
width: '20%',
|
|
15
15
|
value: (user) => user.displayName,
|
|
16
16
|
cell: (user) => <b>{user.displayName}</b>,
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
key: 'email',
|
|
20
20
|
header: t('users.table.email'),
|
|
21
|
-
width: '
|
|
21
|
+
width: '22%',
|
|
22
22
|
value: (user) => user.email,
|
|
23
23
|
cell: (user) => <span>{user.email}</span>,
|
|
24
24
|
},
|
|
@@ -34,7 +34,7 @@ export function memberColumns(
|
|
|
34
34
|
{
|
|
35
35
|
key: 'status',
|
|
36
36
|
header: t('users.table.status'),
|
|
37
|
-
width: '
|
|
37
|
+
width: '22%',
|
|
38
38
|
cell: (user) => <span class="ui-table__state">
|
|
39
39
|
<Tag
|
|
40
40
|
tone={user.membershipStatus === 'active' ? 'success' : 'neutral'}
|
|
@@ -55,11 +55,20 @@ export function memberColumns(
|
|
|
55
55
|
{
|
|
56
56
|
key: 'scopes',
|
|
57
57
|
header: t('users.table.scopes'),
|
|
58
|
-
width: '
|
|
58
|
+
width: '10%',
|
|
59
59
|
numeric: true,
|
|
60
60
|
cell: (user) => new Intl.NumberFormat(activeLocale()).format(
|
|
61
61
|
user.scopes.length,
|
|
62
62
|
),
|
|
63
63
|
},
|
|
64
|
+
/* No `value`: the server's sorted read has no keyset over created_at. */
|
|
65
|
+
{
|
|
66
|
+
key: 'createdAt',
|
|
67
|
+
header: t('users.table.joined'),
|
|
68
|
+
width: '12%',
|
|
69
|
+
cell: (user) => new Intl.DateTimeFormat(activeLocale(), {
|
|
70
|
+
dateStyle: 'medium',
|
|
71
|
+
}).format(user.createdAt),
|
|
72
|
+
},
|
|
64
73
|
];
|
|
65
74
|
}
|
|
@@ -46,6 +46,7 @@ export function createUsersClientState() {
|
|
|
46
46
|
actor: cell<UsersContext['actor'] | null>(null),
|
|
47
47
|
passwordMinLength: 12,
|
|
48
48
|
memberCount: 0,
|
|
49
|
+
ownerCount: 0,
|
|
49
50
|
query: '',
|
|
50
51
|
/** The term the rows on screen answer; typing moves `query` ahead of it. */
|
|
51
52
|
appliedQuery: '',
|
|
@@ -192,6 +193,21 @@ export function replaceMember(
|
|
|
192
193
|
);
|
|
193
194
|
}
|
|
194
195
|
|
|
196
|
+
/**
|
|
197
|
+
* The owner count after one row changed on screen: the count is read whole
|
|
198
|
+
* from the server, so a role change moves it by the difference between the row
|
|
199
|
+
* the page held and the row the server answered.
|
|
200
|
+
*/
|
|
201
|
+
export function ownerCountAfter(
|
|
202
|
+
ownerCount: number,
|
|
203
|
+
previous: TenantMember | undefined,
|
|
204
|
+
next: TenantMember,
|
|
205
|
+
): number {
|
|
206
|
+
const before = previous?.role === 'owner' ? 1 : 0;
|
|
207
|
+
const after = next.role === 'owner' ? 1 : 0;
|
|
208
|
+
return ownerCount - before + after;
|
|
209
|
+
}
|
|
210
|
+
|
|
195
211
|
/* The membership route answers with stable codes; each one gets the sentence
|
|
196
212
|
that says what to do instead, and anything else keeps the server message. */
|
|
197
213
|
export function membershipStatusMessage(error: unknown): string {
|
|
@@ -54,6 +54,8 @@ export interface UsersContext {
|
|
|
54
54
|
readonly passwordMinLength: number;
|
|
55
55
|
/** Every membership of the workspace, counted in the database. */
|
|
56
56
|
readonly memberCount: number;
|
|
57
|
+
/** The memberships holding the owner role, counted the same way. */
|
|
58
|
+
readonly ownerCount: number;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
function actor(principal: AuthPrincipal): AuthActor {
|
|
@@ -105,6 +107,7 @@ export class UsersService {
|
|
|
105
107
|
actor: { accountId: principal.accountId, role: principal.role },
|
|
106
108
|
passwordMinLength: this.#auth.settings.passwordMinLength,
|
|
107
109
|
memberCount: await service.countTenantMembers(principal.tenantId),
|
|
110
|
+
ownerCount: await service.countTenantOwners(principal.tenantId),
|
|
108
111
|
};
|
|
109
112
|
}
|
|
110
113
|
|
|
@@ -35,12 +35,13 @@
|
|
|
35
35
|
"denied.members": "Reading the user directory needs the users.members.read scope.",
|
|
36
36
|
"table.title": "Members",
|
|
37
37
|
"table.caption": "Workspace users",
|
|
38
|
-
"table.count": "{accounts} accounts",
|
|
38
|
+
"table.count": "{accounts} accounts, {owners} owners",
|
|
39
39
|
"table.user": "User",
|
|
40
40
|
"table.email": "Email",
|
|
41
41
|
"table.role": "Role",
|
|
42
42
|
"table.status": "Workspace access",
|
|
43
43
|
"table.scopes": "Scopes",
|
|
44
|
+
"table.joined": "Joined",
|
|
44
45
|
"table.actions": "User actions",
|
|
45
46
|
"table.loading": "Loading workspace accounts…",
|
|
46
47
|
"table.emptyTitle": "No accounts yet",
|
|
@@ -35,12 +35,13 @@
|
|
|
35
35
|
"denied.members": "Odczyt katalogu użytkowników wymaga uprawnienia users.members.read.",
|
|
36
36
|
"table.title": "Członkowie",
|
|
37
37
|
"table.caption": "Użytkownicy przestrzeni roboczej",
|
|
38
|
-
"table.count": "Konta: {accounts}",
|
|
38
|
+
"table.count": "Konta: {accounts}, właściciele: {owners}",
|
|
39
39
|
"table.user": "Użytkownik",
|
|
40
40
|
"table.email": "E-mail",
|
|
41
41
|
"table.role": "Rola",
|
|
42
42
|
"table.status": "Dostęp do przestrzeni",
|
|
43
43
|
"table.scopes": "Zakresy",
|
|
44
|
+
"table.joined": "Dołączył(a)",
|
|
44
45
|
"table.actions": "Akcje użytkownika",
|
|
45
46
|
"table.loading": "Wczytywanie kont…",
|
|
46
47
|
"table.emptyTitle": "Brak kont",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"schemaVersion": 1,
|
|
4
4
|
"id": "workflows.core",
|
|
5
5
|
"package": "@flowdular/module-workflows",
|
|
6
|
-
"version": "0.6.
|
|
6
|
+
"version": "0.6.8",
|
|
7
7
|
"profile": "full",
|
|
8
8
|
"capabilities": [
|
|
9
9
|
"api",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"dependencies": [
|
|
21
21
|
{
|
|
22
22
|
"id": "system.core",
|
|
23
|
-
"range": "^0.
|
|
23
|
+
"range": "^0.8.0"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"id": "auth.core",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
schemaVersion: 1
|
|
2
2
|
id: workflows.core
|
|
3
|
-
specVersion: 0.6.
|
|
3
|
+
specVersion: 0.6.8
|
|
4
4
|
status: approved
|
|
5
5
|
name: Agentic Workflows
|
|
6
6
|
description: Builds, publishes, simulates, and executes durable tenant-scoped DAG workflows whose versioned nodes coordinate pinned agents, deterministic decisions, validated data, and registered module actions.
|
|
@@ -14,7 +14,7 @@ capabilities:
|
|
|
14
14
|
- cli
|
|
15
15
|
dependencies:
|
|
16
16
|
- id: system.core
|
|
17
|
-
range: ^0.
|
|
17
|
+
range: ^0.8.0
|
|
18
18
|
- id: auth.core
|
|
19
19
|
range: ^0.13.0
|
|
20
20
|
- id: agents.core
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
integer,
|
|
4
4
|
runDatabaseMigrations,
|
|
5
5
|
type DatabaseHandle,
|
|
6
|
+
type DatabaseIsolationLevel,
|
|
6
7
|
type DatabaseTransaction,
|
|
7
8
|
} from '@flowdular/sdk/database';
|
|
8
9
|
import type { Actor, UserActor } from '@flowdular/sdk/kernel';
|
|
@@ -868,9 +869,14 @@ export class DatabaseWorkflowsRepository implements WorkflowsRepository {
|
|
|
868
869
|
tenantId: string,
|
|
869
870
|
access: 'read' | 'write',
|
|
870
871
|
body: (transaction: DatabaseTransaction) => Promise<T>,
|
|
872
|
+
isolation?: DatabaseIsolationLevel,
|
|
871
873
|
): Promise<T> {
|
|
872
874
|
await this.readyPromise;
|
|
873
|
-
return this.handles.runtime.transaction(body, {
|
|
875
|
+
return this.handles.runtime.transaction(body, {
|
|
876
|
+
access,
|
|
877
|
+
tenantId,
|
|
878
|
+
...(isolation ? { isolation } : {}),
|
|
879
|
+
});
|
|
874
880
|
}
|
|
875
881
|
|
|
876
882
|
async #query<Row extends object>(
|
|
@@ -1567,21 +1573,34 @@ export class DatabaseWorkflowsRepository implements WorkflowsRepository {
|
|
|
1567
1573
|
tenantId: string,
|
|
1568
1574
|
runId: string,
|
|
1569
1575
|
): Promise<WorkflowRunDetail | null> {
|
|
1570
|
-
|
|
1576
|
+
/* A worker commits a node transition together with its events, so the
|
|
1577
|
+
rebuild only agrees with the rows when every read here shares one
|
|
1578
|
+
snapshot. Read committed would take a new one per statement. */
|
|
1579
|
+
const snapshot = await this.#tx(
|
|
1571
1580
|
tenantId,
|
|
1572
1581
|
'read',
|
|
1573
|
-
async (transaction) =>
|
|
1574
|
-
(
|
|
1582
|
+
async (transaction) => {
|
|
1583
|
+
const row = (
|
|
1575
1584
|
await this.#query<RunRow>(transaction, SQL.getRun, [tenantId, runId])
|
|
1576
|
-
)[0]
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1585
|
+
)[0];
|
|
1586
|
+
if (!row) return null;
|
|
1587
|
+
return {
|
|
1588
|
+
row,
|
|
1589
|
+
events: await this.#eventsIn(
|
|
1590
|
+
transaction,
|
|
1591
|
+
tenantId,
|
|
1592
|
+
runId,
|
|
1593
|
+
0,
|
|
1594
|
+
WORKFLOW_LIMITS.maxRunEvents,
|
|
1595
|
+
),
|
|
1596
|
+
nodes: await this.#nodeStatesIn(transaction, tenantId, runId),
|
|
1597
|
+
edges: await this.#edgeTransfersIn(transaction, tenantId, runId),
|
|
1598
|
+
};
|
|
1599
|
+
},
|
|
1600
|
+
'repeatable-read',
|
|
1584
1601
|
);
|
|
1602
|
+
if (!snapshot) return null;
|
|
1603
|
+
const { row, events, nodes, edges } = snapshot;
|
|
1585
1604
|
const projection = projectWorkflowRunEvents(events);
|
|
1586
1605
|
if (projection.status !== row.status) {
|
|
1587
1606
|
throw new WorkflowEventProjectionError(
|
|
@@ -1589,7 +1608,6 @@ export class DatabaseWorkflowsRepository implements WorkflowsRepository {
|
|
|
1589
1608
|
`Workflow run projection is ${row.status} but its event stream rebuilds as ${projection.status}.`,
|
|
1590
1609
|
);
|
|
1591
1610
|
}
|
|
1592
|
-
const nodes = await this.readNodeStates(tenantId, runId);
|
|
1593
1611
|
for (const [nodeId, status] of Object.entries(projection.nodeStatuses)) {
|
|
1594
1612
|
if (nodes.find((node) => node.nodeId === nodeId)?.status !== status) {
|
|
1595
1613
|
throw new WorkflowEventProjectionError(
|
|
@@ -1612,7 +1630,6 @@ export class DatabaseWorkflowsRepository implements WorkflowsRepository {
|
|
|
1612
1630
|
);
|
|
1613
1631
|
}
|
|
1614
1632
|
}
|
|
1615
|
-
const edges = await this.readEdgeTransfers(tenantId, runId);
|
|
1616
1633
|
for (const [edgeId, state] of Object.entries(projection.edgeStates)) {
|
|
1617
1634
|
if (edges.find((edge) => edge.edgeId === edgeId)?.state !== state) {
|
|
1618
1635
|
throw new WorkflowEventProjectionError(
|
|
@@ -1826,21 +1843,28 @@ export class DatabaseWorkflowsRepository implements WorkflowsRepository {
|
|
|
1826
1843
|
afterSequence: number,
|
|
1827
1844
|
limit: number,
|
|
1828
1845
|
): Promise<readonly WorkflowRunEventV1[]> {
|
|
1829
|
-
return this.#tx(tenantId, 'read',
|
|
1830
|
-
(
|
|
1831
|
-
await this.#query<EventRow>(transaction, SQL.readEvents, [
|
|
1832
|
-
tenantId,
|
|
1833
|
-
runId,
|
|
1834
|
-
Math.max(0, Math.trunc(afterSequence)),
|
|
1835
|
-
Math.min(
|
|
1836
|
-
Math.max(1, Math.trunc(limit)),
|
|
1837
|
-
WORKFLOW_LIMITS.maxRunEvents,
|
|
1838
|
-
),
|
|
1839
|
-
])
|
|
1840
|
-
).map(eventFromRow),
|
|
1846
|
+
return this.#tx(tenantId, 'read', (transaction) =>
|
|
1847
|
+
this.#eventsIn(transaction, tenantId, runId, afterSequence, limit),
|
|
1841
1848
|
);
|
|
1842
1849
|
}
|
|
1843
1850
|
|
|
1851
|
+
async #eventsIn(
|
|
1852
|
+
transaction: DatabaseTransaction,
|
|
1853
|
+
tenantId: string,
|
|
1854
|
+
runId: string,
|
|
1855
|
+
afterSequence: number,
|
|
1856
|
+
limit: number,
|
|
1857
|
+
): Promise<readonly WorkflowRunEventV1[]> {
|
|
1858
|
+
return (
|
|
1859
|
+
await this.#query<EventRow>(transaction, SQL.readEvents, [
|
|
1860
|
+
tenantId,
|
|
1861
|
+
runId,
|
|
1862
|
+
Math.max(0, Math.trunc(afterSequence)),
|
|
1863
|
+
Math.min(Math.max(1, Math.trunc(limit)), WORKFLOW_LIMITS.maxRunEvents),
|
|
1864
|
+
])
|
|
1865
|
+
).map(eventFromRow);
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1844
1868
|
async startAttempt(
|
|
1845
1869
|
write: StartAttemptWrite,
|
|
1846
1870
|
actor: Actor,
|
|
@@ -2516,42 +2540,58 @@ export class DatabaseWorkflowsRepository implements WorkflowsRepository {
|
|
|
2516
2540
|
tenantId: string,
|
|
2517
2541
|
runId: string,
|
|
2518
2542
|
): Promise<readonly WorkflowNodeExecution[]> {
|
|
2519
|
-
return this.#tx(tenantId, 'read',
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2543
|
+
return this.#tx(tenantId, 'read', (transaction) =>
|
|
2544
|
+
this.#nodeStatesIn(transaction, tenantId, runId),
|
|
2545
|
+
);
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
async #nodeStatesIn(
|
|
2549
|
+
transaction: DatabaseTransaction,
|
|
2550
|
+
tenantId: string,
|
|
2551
|
+
runId: string,
|
|
2552
|
+
): Promise<readonly WorkflowNodeExecution[]> {
|
|
2553
|
+
const states = await this.#query<NodeStateRow>(
|
|
2554
|
+
transaction,
|
|
2555
|
+
SQL.readNodeStates,
|
|
2556
|
+
[tenantId, runId],
|
|
2557
|
+
);
|
|
2558
|
+
const attempts = await this.#query<AttemptRow>(
|
|
2559
|
+
transaction,
|
|
2560
|
+
SQL.readAttempts,
|
|
2561
|
+
[tenantId, runId],
|
|
2562
|
+
);
|
|
2563
|
+
return states.map((state) =>
|
|
2564
|
+
nodeExecutionFromRow(
|
|
2565
|
+
state,
|
|
2566
|
+
attempts
|
|
2567
|
+
.filter((attempt) => attempt.node_id === state.node_id)
|
|
2568
|
+
.map(attemptFromRow),
|
|
2569
|
+
),
|
|
2570
|
+
);
|
|
2539
2571
|
}
|
|
2540
2572
|
|
|
2541
2573
|
async readEdgeTransfers(
|
|
2542
2574
|
tenantId: string,
|
|
2543
2575
|
runId: string,
|
|
2544
2576
|
): Promise<readonly WorkflowEdgeTransfer[]> {
|
|
2545
|
-
return this.#tx(tenantId, 'read',
|
|
2546
|
-
(
|
|
2547
|
-
await this.#query<EdgeRow>(transaction, SQL.readEdgeTransfers, [
|
|
2548
|
-
tenantId,
|
|
2549
|
-
runId,
|
|
2550
|
-
])
|
|
2551
|
-
).map(edgeFromRow),
|
|
2577
|
+
return this.#tx(tenantId, 'read', (transaction) =>
|
|
2578
|
+
this.#edgeTransfersIn(transaction, tenantId, runId),
|
|
2552
2579
|
);
|
|
2553
2580
|
}
|
|
2554
2581
|
|
|
2582
|
+
async #edgeTransfersIn(
|
|
2583
|
+
transaction: DatabaseTransaction,
|
|
2584
|
+
tenantId: string,
|
|
2585
|
+
runId: string,
|
|
2586
|
+
): Promise<readonly WorkflowEdgeTransfer[]> {
|
|
2587
|
+
return (
|
|
2588
|
+
await this.#query<EdgeRow>(transaction, SQL.readEdgeTransfers, [
|
|
2589
|
+
tenantId,
|
|
2590
|
+
runId,
|
|
2591
|
+
])
|
|
2592
|
+
).map(edgeFromRow);
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2555
2595
|
/* The chain order is the sequence; the time order rides the index 0001 made
|
|
2556
2596
|
for it, with the sequence as the tie breaker in both. */
|
|
2557
2597
|
async listAudit(
|
package/package.json
CHANGED
|
@@ -13,6 +13,10 @@ import { CommandPalette } from './shell/CommandPalette.tsrx';
|
|
|
13
13
|
import { widgetsForSlot } from './shell/ContributionOutlets.tsrx';
|
|
14
14
|
import { registerModuleTranslations, useTranslation } from './i18n/runtime.ts';
|
|
15
15
|
import { MobileHeader } from './shell/MobileHeader.tsrx';
|
|
16
|
+
import {
|
|
17
|
+
contributionsForActiveModules,
|
|
18
|
+
loadActiveModules,
|
|
19
|
+
} from './shell/modules.ts';
|
|
16
20
|
import {
|
|
17
21
|
coreNavigation,
|
|
18
22
|
navigationForIdentity,
|
|
@@ -44,10 +48,6 @@ export function ApplicationShell(props: ApplicationShellProps) @{
|
|
|
44
48
|
locale is side-effect free for subscribers. */
|
|
45
49
|
registerModuleTranslations(props.contributions ?? []);
|
|
46
50
|
const { t } = useTranslation();
|
|
47
|
-
const registry = useMemo(
|
|
48
|
-
() => createClientContributionRegistry(props.contributions ?? []),
|
|
49
|
-
[props.contributions],
|
|
50
|
-
);
|
|
51
51
|
const shell = useMemo(() => createShellState(props.initialView), [
|
|
52
52
|
props.initialView,
|
|
53
53
|
]);
|
|
@@ -60,12 +60,28 @@ export function ApplicationShell(props: ApplicationShellProps) @{
|
|
|
60
60
|
? null
|
|
61
61
|
: props.identity.tenantId,
|
|
62
62
|
),
|
|
63
|
+
/* The workspace whose active modules are known, and which they are;
|
|
64
|
+
null ids show every module, so a failed read never empties the shell. */
|
|
65
|
+
activationTenantId: cell<string | null>(null),
|
|
66
|
+
activeModules: cell<readonly string[] | null>(null),
|
|
63
67
|
}),
|
|
64
68
|
[],
|
|
65
69
|
);
|
|
66
70
|
const [initializedTenant, setInitializedTenant] = useValue(
|
|
67
71
|
initialization.state.tenantId,
|
|
68
72
|
);
|
|
73
|
+
const [activationTenant, setActivationTenant] = useValue(
|
|
74
|
+
initialization.state.activationTenantId,
|
|
75
|
+
);
|
|
76
|
+
const [activeModules, setActiveModules] = useValue(
|
|
77
|
+
initialization.state.activeModules,
|
|
78
|
+
);
|
|
79
|
+
const registry = useMemo(
|
|
80
|
+
() => createClientContributionRegistry(
|
|
81
|
+
contributionsForActiveModules(props.contributions ?? [], activeModules),
|
|
82
|
+
),
|
|
83
|
+
[props.contributions, activeModules],
|
|
84
|
+
);
|
|
69
85
|
const [activeView, setActiveView] = useValue(shell.state.activeView);
|
|
70
86
|
const [commandOpen, setCommandOpen] = useValue(shell.state.commandOpen);
|
|
71
87
|
const [navigationOpen, setNavigationOpen] = useValue(
|
|
@@ -80,7 +96,9 @@ export function ApplicationShell(props: ApplicationShellProps) @{
|
|
|
80
96
|
const [query, setQuery] = useValue(shell.state.query);
|
|
81
97
|
const [tenantBusy, setTenantBusy] = useValue(shell.state.tenantBusy);
|
|
82
98
|
const [tenantError, setTenantError] = useValue(shell.state.tenantError);
|
|
83
|
-
const workspaceInitialized =
|
|
99
|
+
const workspaceInitialized =
|
|
100
|
+
initializedTenant === props.identity.tenantId &&
|
|
101
|
+
activationTenant === props.identity.tenantId;
|
|
84
102
|
const grantedScopes = new Set(props.identity.scopes);
|
|
85
103
|
const visibleNavigation = navigationForIdentity(
|
|
86
104
|
[...coreNavigation(t), ...registry.navigation],
|
|
@@ -222,6 +240,29 @@ export function ApplicationShell(props: ApplicationShellProps) @{
|
|
|
222
240
|
return () => controller.abort();
|
|
223
241
|
}, [props.identity.accountId, props.identity.tenantId]);
|
|
224
242
|
|
|
243
|
+
useEffect(() => {
|
|
244
|
+
const controller = new AbortController();
|
|
245
|
+
setActivationTenant(null);
|
|
246
|
+
const resolveActiveModules = props.loadActiveModules ?? loadActiveModules;
|
|
247
|
+
void resolveActiveModules({
|
|
248
|
+
accountId: props.identity.accountId,
|
|
249
|
+
tenantId: props.identity.tenantId,
|
|
250
|
+
signal: controller.signal,
|
|
251
|
+
}).then(
|
|
252
|
+
(ids) => {
|
|
253
|
+
if (controller.signal.aborted) return;
|
|
254
|
+
setActiveModules(ids);
|
|
255
|
+
setActivationTenant(props.identity.tenantId);
|
|
256
|
+
},
|
|
257
|
+
() => {
|
|
258
|
+
if (controller.signal.aborted) return;
|
|
259
|
+
setActiveModules(null);
|
|
260
|
+
setActivationTenant(props.identity.tenantId);
|
|
261
|
+
},
|
|
262
|
+
);
|
|
263
|
+
return () => controller.abort();
|
|
264
|
+
}, [props.identity.accountId, props.identity.tenantId]);
|
|
265
|
+
|
|
225
266
|
useEffect(() => {
|
|
226
267
|
try {
|
|
227
268
|
if (localStorage.getItem(SIDEBAR_STORAGE_KEY) === 'rail') {
|
|
@@ -25,6 +25,11 @@ export type {
|
|
|
25
25
|
WorkspaceSlot,
|
|
26
26
|
} from './contributions.ts';
|
|
27
27
|
export { openCommandPalette } from './shell/command.ts';
|
|
28
|
+
export {
|
|
29
|
+
contributionsForActiveModules,
|
|
30
|
+
loadActiveModules,
|
|
31
|
+
} from './shell/modules.ts';
|
|
32
|
+
export type { ActiveModulesLoader } from './shell/modules.ts';
|
|
28
33
|
export {
|
|
29
34
|
COMMAND_SEARCH_DEBOUNCE_MS,
|
|
30
35
|
COMMAND_SEARCH_MINIMUM,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { REQUIRED_MODULE_IDS } from '@flowdular/sdk/contracts';
|
|
2
|
+
import type {
|
|
3
|
+
ModuleClientContribution,
|
|
4
|
+
ModuleClientInitializationContext,
|
|
5
|
+
} from '../contributions.ts';
|
|
6
|
+
|
|
7
|
+
const REQUIRED = new Set<string>(REQUIRED_MODULE_IDS);
|
|
8
|
+
|
|
9
|
+
export type ActiveModulesLoader = (
|
|
10
|
+
context: ModuleClientInitializationContext,
|
|
11
|
+
) => Promise<readonly string[] | null>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The contributions the workspace may show: every required module, and the
|
|
15
|
+
* optional ones the workspace has active. `null` means the activation could
|
|
16
|
+
* not be read, and the shell shows everything rather than nothing; the server
|
|
17
|
+
* gate still refuses the API of an inactive module.
|
|
18
|
+
*/
|
|
19
|
+
export function contributionsForActiveModules(
|
|
20
|
+
contributions: readonly ModuleClientContribution[],
|
|
21
|
+
activeModules: readonly string[] | null,
|
|
22
|
+
): readonly ModuleClientContribution[] {
|
|
23
|
+
if (activeModules === null) return contributions;
|
|
24
|
+
const active = new Set(activeModules);
|
|
25
|
+
return contributions.filter(
|
|
26
|
+
(contribution) =>
|
|
27
|
+
REQUIRED.has(contribution.moduleId) || active.has(contribution.moduleId),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* system.core answers the active composed module ids to any member of the
|
|
32
|
+
workspace; an application without it, or a failed read, answers null. */
|
|
33
|
+
export const loadActiveModules: ActiveModulesLoader = async (context) => {
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch('/api/system/modules/active', {
|
|
36
|
+
headers: { accept: 'application/json' },
|
|
37
|
+
credentials: 'same-origin',
|
|
38
|
+
signal: context.signal,
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok) return null;
|
|
41
|
+
const body = (await response.json()) as { modules?: unknown };
|
|
42
|
+
return Array.isArray(body.modules) &&
|
|
43
|
+
body.modules.every((id) => typeof id === 'string')
|
|
44
|
+
? (body.modules as string[])
|
|
45
|
+
: null;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ModuleClientContribution } from '../contributions.ts';
|
|
2
2
|
import type { ShellView } from '../state.ts';
|
|
3
|
+
import type { ActiveModulesLoader } from './modules.ts';
|
|
3
4
|
|
|
4
5
|
export interface ShellTenant {
|
|
5
6
|
readonly tenantId: string;
|
|
@@ -22,6 +23,8 @@ export interface ApplicationShellProps {
|
|
|
22
23
|
initialView?: ShellView;
|
|
23
24
|
contributions?: readonly ModuleClientContribution[];
|
|
24
25
|
identity: ShellIdentity;
|
|
26
|
+
/** Reads the active module ids of the workspace; defaults to system.core's endpoint. */
|
|
27
|
+
loadActiveModules?: ActiveModulesLoader;
|
|
25
28
|
onSignOut: () => void | Promise<void>;
|
|
26
29
|
onSwitchTenant: (tenantId: string) => void | Promise<void>;
|
|
27
30
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
removed or changed. The surface is pinned by
|
|
5
5
|
packages/kernel/platform-api.snapshot.d.ts; `pnpm platform-api:check` fails
|
|
6
6
|
when the surface changes without a bump here. */
|
|
7
|
-
export const PLATFORM_API_VERSION = '0.1.
|
|
7
|
+
export const PLATFORM_API_VERSION = '0.1.9';
|
|
8
8
|
|
|
9
9
|
/* The workspace time zone: one tenant setting, declared by the module named
|
|
10
10
|
here and read by any module that shows or schedules a local time. The id, the
|
|
@@ -15,6 +15,16 @@ export const TENANT_TIME_ZONE_SETTING = {
|
|
|
15
15
|
defaultValue: 'UTC',
|
|
16
16
|
} as const;
|
|
17
17
|
|
|
18
|
+
/* Modules every workspace needs to sign in, hold members and show the shell.
|
|
19
|
+
They are composed like any other module but never deactivated per workspace,
|
|
20
|
+
and the endpoint gate never asks about them. */
|
|
21
|
+
export const REQUIRED_MODULE_IDS = [
|
|
22
|
+
'system.core',
|
|
23
|
+
'auth.core',
|
|
24
|
+
'users.core',
|
|
25
|
+
'profile.core',
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
18
28
|
export type ModuleCapability =
|
|
19
29
|
| 'api'
|
|
20
30
|
| 'database'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ServerRoute, type Context } from '@octanejs/app-core';
|
|
2
2
|
import { serverLogger } from './log.ts';
|
|
3
3
|
import { serverMetrics } from './metrics.ts';
|
|
4
|
+
import { MODULE_INACTIVE, routeActiveForTenant } from './module-activation.ts';
|
|
4
5
|
import {
|
|
5
6
|
formatTraceParent,
|
|
6
7
|
runWithTrace,
|
|
@@ -11,6 +12,8 @@ import { serverTracer, type Span } from './trace/tracer.ts';
|
|
|
11
12
|
export interface EndpointIdentity {
|
|
12
13
|
readonly subjectId: string;
|
|
13
14
|
readonly permissions: ReadonlySet<string>;
|
|
15
|
+
/** The workspace the identity acts in; the module activation gate reads it. */
|
|
16
|
+
readonly tenantId?: string;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export interface EndpointExecutionContext {
|
|
@@ -131,6 +134,19 @@ export function defineEndpoint(
|
|
|
131
134
|
traceparent,
|
|
132
135
|
);
|
|
133
136
|
}
|
|
137
|
+
if (
|
|
138
|
+
identity.tenantId !== undefined &&
|
|
139
|
+
!(await routeActiveForTenant(serverRoute, identity.tenantId))
|
|
140
|
+
) {
|
|
141
|
+
status = 403;
|
|
142
|
+
return problem(
|
|
143
|
+
403,
|
|
144
|
+
MODULE_INACTIVE,
|
|
145
|
+
'This module is inactive in the workspace.',
|
|
146
|
+
requestId,
|
|
147
|
+
traceparent,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
134
150
|
}
|
|
135
151
|
|
|
136
152
|
const response = await definition.handler({
|
|
@@ -171,7 +187,7 @@ export function defineEndpoint(
|
|
|
171
187
|
}
|
|
172
188
|
};
|
|
173
189
|
|
|
174
|
-
const serverRoute = new ServerRoute({
|
|
190
|
+
const serverRoute: ServerRoute = new ServerRoute({
|
|
175
191
|
path: definition.path,
|
|
176
192
|
methods: definition.methods.map((method) => method.toUpperCase()),
|
|
177
193
|
handler: (context) => {
|
|
@@ -202,3 +202,13 @@ export type {
|
|
|
202
202
|
ModuleServerContext,
|
|
203
203
|
ModuleServerComposition,
|
|
204
204
|
} from './composition.ts';
|
|
205
|
+
export {
|
|
206
|
+
bindModuleCompositions,
|
|
207
|
+
bindModuleRoutes,
|
|
208
|
+
installModuleActivationGate,
|
|
209
|
+
isRequiredModule,
|
|
210
|
+
MODULE_INACTIVE,
|
|
211
|
+
moduleOfRoute,
|
|
212
|
+
routeActiveForTenant,
|
|
213
|
+
} from './module-activation.ts';
|
|
214
|
+
export type { ModuleActivationGate } from './module-activation.ts';
|