@flowdular/sdk 0.3.0 → 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 +4 -2
- package/.ai/policies/capabilities.yaml +30 -3
- 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/.ai/skills/cli-extension/SKILL.md +1 -1
- package/.ai/skills/deploy-operate/SKILL.md +7 -2
- 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/migrations/0005_approvals_grant_audit.down.sql +4 -0
- package/modules/approvals/migrations/0005_approvals_grant_audit.up.sql +20 -0
- package/modules/approvals/module.json +2 -2
- package/modules/approvals/package.json +1 -1
- package/modules/approvals/spec/module.yaml +12 -3
- package/modules/approvals/src/domain/capability.ts +12 -0
- package/modules/approvals/src/domain/grant.ts +69 -0
- package/modules/approvals/src/domain/types.ts +14 -0
- package/modules/approvals/src/index.ts +9 -0
- package/modules/approvals/src/platform.ts +8 -0
- package/modules/approvals/src/server/runtime.ts +4 -0
- package/modules/approvals/src/services/approvals-service.ts +80 -0
- package/modules/approvals/src/services/database-repository.ts +65 -6
- package/modules/approvals/src/services/migration.ts +34 -0
- package/modules/approvals/src/services/repository.ts +8 -0
- 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/migrations/0003_connectors_rotation_inventory.down.sql +2 -0
- package/modules/connectors/migrations/0003_connectors_rotation_inventory.up.sql +19 -0
- package/modules/connectors/module.json +8 -4
- package/modules/connectors/package.json +2 -1
- package/modules/connectors/spec/module.yaml +3 -2
- package/modules/connectors/src/cli/commands.json +17 -0
- package/modules/connectors/src/cli/index.ts +126 -0
- package/modules/connectors/src/services/credential-rotation.ts +221 -0
- package/modules/connectors/src/services/credential-vault.ts +6 -0
- package/modules/connectors/src/services/migration.ts +36 -0
- package/modules/directory/module.json +1 -1
- package/modules/directory/spec/module.yaml +1 -1
- package/modules/documents/migrations/0003_documents_rotation_inventory.down.sql +2 -0
- package/modules/documents/migrations/0003_documents_rotation_inventory.up.sql +18 -0
- package/modules/documents/module.json +8 -4
- package/modules/documents/package.json +2 -1
- package/modules/documents/spec/module.yaml +3 -2
- package/modules/documents/src/cli/commands.json +17 -0
- package/modules/documents/src/cli/index.ts +145 -0
- package/modules/documents/src/services/database-repository.ts +15 -4
- package/modules/documents/src/services/documents-service.ts +13 -9
- package/modules/documents/src/services/migration.ts +35 -0
- package/modules/documents/src/services/repository.ts +12 -2
- package/modules/documents/src/services/storage-rotation.ts +157 -0
- package/modules/exports/migrations/0003_exports_rotation_inventory.down.sql +1 -0
- package/modules/exports/migrations/0003_exports_rotation_inventory.up.sql +9 -0
- package/modules/exports/module.json +8 -4
- package/modules/exports/package.json +2 -1
- package/modules/exports/spec/module.yaml +3 -2
- package/modules/exports/src/cli/commands.json +17 -0
- package/modules/exports/src/cli/index.ts +145 -0
- package/modules/exports/src/server/index.ts +0 -1
- package/modules/exports/src/services/data-classes.ts +16 -13
- package/modules/exports/src/services/database-repository.ts +30 -32
- package/modules/exports/src/services/migration.ts +27 -0
- package/modules/exports/src/services/repository.ts +9 -10
- package/modules/exports/src/services/storage-rotation.ts +138 -0
- 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/database/src/backup.ts +1 -0
- package/packages/database/src/migrations.ts +7 -0
- package/packages/harness/src/runtime.ts +169 -10
- package/packages/harness/src/tool-adapters.ts +6 -13
- package/packages/kernel/src/approval-grant.ts +310 -0
- package/packages/kernel/src/index.ts +20 -0
- 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/packages/storage/src/envelope.ts +70 -21
- package/packages/storage/src/index.ts +7 -1
- package/packages/storage/src/port.ts +12 -1
- package/packages/storage/src/reseal.ts +128 -0
- package/assets/flowdular-banner.png +0 -0
|
@@ -2,14 +2,54 @@ import type {
|
|
|
2
2
|
PlatformServerComposition,
|
|
3
3
|
PlatformServerContext,
|
|
4
4
|
} from '@flowdular/sdk/modules/auth/server';
|
|
5
|
+
import { installModuleActivationGate } from '@flowdular/sdk/server';
|
|
6
|
+
import {
|
|
7
|
+
composedModules,
|
|
8
|
+
SYSTEM_MODULES_CAPABILITY,
|
|
9
|
+
type ComposedModule,
|
|
10
|
+
} from './domain/modules.ts';
|
|
5
11
|
import { createSystemRoutes } from './server/endpoints.ts';
|
|
12
|
+
import { readModuleCatalog } from './server/module-catalog.ts';
|
|
13
|
+
import { createSystemRuntime } from './server/runtime.ts';
|
|
14
|
+
import type { SystemModulesCapability } from './server/capability.ts';
|
|
6
15
|
import { SYSTEM_MODULE_SETTINGS } from './settings.ts';
|
|
7
16
|
|
|
8
17
|
export function createServerComposition(
|
|
9
18
|
context: PlatformServerContext,
|
|
10
19
|
): PlatformServerComposition {
|
|
20
|
+
/* The composed set is baked at build, so the manifests are read once per
|
|
21
|
+
composition, which in development is once per reload. */
|
|
22
|
+
let composed: readonly ComposedModule[] | null = null;
|
|
23
|
+
const modules = () =>
|
|
24
|
+
(composed ??= composedModules(
|
|
25
|
+
readModuleCatalog(context.workspaceRoot).filter((entry) => entry.enabled),
|
|
26
|
+
));
|
|
27
|
+
const runtime = createSystemRuntime({
|
|
28
|
+
databases: context.databases,
|
|
29
|
+
purpose:
|
|
30
|
+
context.environment.NODE_ENV === 'test'
|
|
31
|
+
? 'test'
|
|
32
|
+
: context.environment.NODE_ENV === 'production'
|
|
33
|
+
? 'runtime'
|
|
34
|
+
: 'preview',
|
|
35
|
+
modules,
|
|
36
|
+
audit: async (actor, change) =>
|
|
37
|
+
(await context.auth.service()).recordModuleActivation(actor, change),
|
|
38
|
+
});
|
|
39
|
+
const capability: SystemModulesCapability = {
|
|
40
|
+
isActive: async (tenantId, moduleId) =>
|
|
41
|
+
(await runtime.service()).isActive(tenantId, moduleId),
|
|
42
|
+
activeIds: async (tenantId) =>
|
|
43
|
+
(await runtime.service()).activeIds(tenantId),
|
|
44
|
+
};
|
|
45
|
+
context.capabilities.register(SYSTEM_MODULES_CAPABILITY, capability);
|
|
46
|
+
installModuleActivationGate(capability);
|
|
11
47
|
return {
|
|
12
|
-
routes: createSystemRoutes(context),
|
|
48
|
+
routes: createSystemRoutes({ ...context, activation: runtime }),
|
|
13
49
|
settings: SYSTEM_MODULE_SETTINGS,
|
|
50
|
+
dispose: async () => {
|
|
51
|
+
installModuleActivationGate(null);
|
|
52
|
+
await runtime.dispose();
|
|
53
|
+
},
|
|
14
54
|
};
|
|
15
55
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** `system.modules.v1`: which composed modules are active in a workspace. */
|
|
2
|
+
export interface SystemModulesCapability {
|
|
3
|
+
isActive(tenantId: string, moduleId: string): Promise<boolean>;
|
|
4
|
+
/** Sorted ids of the composed modules active in the workspace. */
|
|
5
|
+
activeIds(tenantId: string): Promise<readonly string[]>;
|
|
6
|
+
}
|
|
@@ -26,12 +26,57 @@ import {
|
|
|
26
26
|
SYSTEM_MODULE_ID,
|
|
27
27
|
TENANT_TIME_ZONE_KEY,
|
|
28
28
|
} from '../domain/time-zone.ts';
|
|
29
|
-
import {
|
|
29
|
+
import type { ModuleActivationEntry } from '../domain/modules.ts';
|
|
30
|
+
import { ModuleActivationError } from '../services/module-activation-service.ts';
|
|
31
|
+
import {
|
|
32
|
+
readModuleCatalog,
|
|
33
|
+
type ModuleCatalogEntry,
|
|
34
|
+
} from './module-catalog.ts';
|
|
35
|
+
import type { SystemRuntime } from './runtime.ts';
|
|
30
36
|
|
|
31
37
|
export interface SystemRouteOptions {
|
|
32
38
|
readonly workspaceRoot: string;
|
|
33
39
|
readonly auth: AuthRuntime;
|
|
34
40
|
readonly settings: ModuleSettingsRuntime;
|
|
41
|
+
/** Per-workspace activation of the composed modules. */
|
|
42
|
+
readonly activation: SystemRuntime;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A catalog row with its activation in the asking principal's workspace. */
|
|
46
|
+
export interface ModuleCatalogPayload extends ModuleCatalogEntry {
|
|
47
|
+
readonly active: boolean;
|
|
48
|
+
readonly optional: boolean;
|
|
49
|
+
readonly dependents: readonly string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* A module the workspace holds but the application does not compose has no
|
|
53
|
+
activation to speak of: it is listed as available and never active. */
|
|
54
|
+
function catalogPayload(
|
|
55
|
+
entry: ModuleCatalogEntry,
|
|
56
|
+
activation: ModuleActivationEntry | undefined,
|
|
57
|
+
): ModuleCatalogPayload {
|
|
58
|
+
return {
|
|
59
|
+
...entry,
|
|
60
|
+
active: activation?.active ?? false,
|
|
61
|
+
optional: activation?.optional ?? true,
|
|
62
|
+
dependents: activation?.dependents ?? [],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function activationProblem(error: unknown): Response {
|
|
67
|
+
if (error instanceof ModuleActivationError) {
|
|
68
|
+
return jsonResponse(
|
|
69
|
+
{
|
|
70
|
+
error: {
|
|
71
|
+
code: error.code,
|
|
72
|
+
message: error.message,
|
|
73
|
+
modules: error.modules,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
error.status,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return problemResponse(error, 'The module activation request failed.');
|
|
35
80
|
}
|
|
36
81
|
|
|
37
82
|
const MAIL_TRANSPORT_REQUIRED =
|
|
@@ -236,8 +281,9 @@ async function readActivity(
|
|
|
236
281
|
return keys.map((date) => ({ date, count: counts.get(date) ?? 0 }));
|
|
237
282
|
}
|
|
238
283
|
|
|
239
|
-
/*
|
|
240
|
-
|
|
284
|
+
/* Manifests and specifications from the workspace, with the per-workspace
|
|
285
|
+
activation of the composed ones. Enabling or disabling a module stays a CLI
|
|
286
|
+
operation with its own approval trail. */
|
|
241
287
|
export function createSystemRoutes(options: SystemRouteOptions) {
|
|
242
288
|
const modules = defineEndpoint({
|
|
243
289
|
id: 'system.modules.list',
|
|
@@ -245,10 +291,18 @@ export function createSystemRoutes(options: SystemRouteOptions) {
|
|
|
245
291
|
methods: ['GET'],
|
|
246
292
|
access: { kind: 'permission', permission: SYSTEM_PERMISSIONS.modulesRead },
|
|
247
293
|
resolveIdentity: endpointIdentityFromContext,
|
|
248
|
-
handler: () => {
|
|
294
|
+
handler: async ({ octane }) => {
|
|
249
295
|
try {
|
|
296
|
+
const principal = principalFromContext(octane)!;
|
|
297
|
+
const activations = new Map(
|
|
298
|
+
(
|
|
299
|
+
await (await options.activation.service()).list(principal.tenantId)
|
|
300
|
+
).map((entry) => [entry.id, entry]),
|
|
301
|
+
);
|
|
250
302
|
return jsonResponse({
|
|
251
|
-
modules: readModuleCatalog(options.workspaceRoot)
|
|
303
|
+
modules: readModuleCatalog(options.workspaceRoot).map((entry) =>
|
|
304
|
+
catalogPayload(entry, activations.get(entry.id)),
|
|
305
|
+
),
|
|
252
306
|
commands: {
|
|
253
307
|
enable: 'pnpm flowdular module enable <id> --apply',
|
|
254
308
|
disable: 'pnpm flowdular module disable <id> --apply',
|
|
@@ -263,6 +317,66 @@ export function createSystemRoutes(options: SystemRouteOptions) {
|
|
|
263
317
|
},
|
|
264
318
|
});
|
|
265
319
|
|
|
320
|
+
const activeModules = defineEndpoint({
|
|
321
|
+
id: 'system.modules.active',
|
|
322
|
+
path: '/api/system/modules/active',
|
|
323
|
+
methods: ['GET'],
|
|
324
|
+
access: {
|
|
325
|
+
kind: 'permission',
|
|
326
|
+
permission: SYSTEM_PERMISSIONS.workspaceAccess,
|
|
327
|
+
},
|
|
328
|
+
resolveIdentity: endpointIdentityFromContext,
|
|
329
|
+
handler: async ({ octane }) => {
|
|
330
|
+
try {
|
|
331
|
+
const principal = principalFromContext(octane)!;
|
|
332
|
+
const service = await options.activation.service();
|
|
333
|
+
return jsonResponse({
|
|
334
|
+
modules: await service.activeIds(principal.tenantId),
|
|
335
|
+
});
|
|
336
|
+
} catch (error) {
|
|
337
|
+
return activationProblem(error);
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const activation = (id: string, path: string, active: boolean) =>
|
|
343
|
+
defineEndpoint({
|
|
344
|
+
id,
|
|
345
|
+
path,
|
|
346
|
+
methods: ['POST'],
|
|
347
|
+
access: {
|
|
348
|
+
kind: 'permission',
|
|
349
|
+
permission: SYSTEM_PERMISSIONS.settingsManage,
|
|
350
|
+
},
|
|
351
|
+
resolveIdentity: endpointIdentityFromContext,
|
|
352
|
+
handler: async ({ octane }) => {
|
|
353
|
+
const denial = sessionMutationDenial(octane, options.auth);
|
|
354
|
+
if (denial) return denial;
|
|
355
|
+
try {
|
|
356
|
+
const principal = principalFromContext(octane)!;
|
|
357
|
+
const body = await readJsonObject(octane.request);
|
|
358
|
+
const moduleId = requiredString(body, 'moduleId', { max: 128 });
|
|
359
|
+
const service = await options.activation.service();
|
|
360
|
+
const entry = active
|
|
361
|
+
? await service.activate(principal, moduleId)
|
|
362
|
+
: await service.deactivate(principal, moduleId);
|
|
363
|
+
return jsonResponse({ module: entry });
|
|
364
|
+
} catch (error) {
|
|
365
|
+
return activationProblem(error);
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
const activateModule = activation(
|
|
370
|
+
'system.modules.activate',
|
|
371
|
+
'/api/system/modules/activate',
|
|
372
|
+
true,
|
|
373
|
+
);
|
|
374
|
+
const deactivateModule = activation(
|
|
375
|
+
'system.modules.deactivate',
|
|
376
|
+
'/api/system/modules/deactivate',
|
|
377
|
+
false,
|
|
378
|
+
);
|
|
379
|
+
|
|
266
380
|
/* Module names come from the workspace specs; the catalog is read once per
|
|
267
381
|
composition, which in development is once per reload. */
|
|
268
382
|
let names: ReadonlyMap<
|
|
@@ -409,6 +523,9 @@ export function createSystemRoutes(options: SystemRouteOptions) {
|
|
|
409
523
|
|
|
410
524
|
return [
|
|
411
525
|
modules.serverRoute,
|
|
526
|
+
activeModules.serverRoute,
|
|
527
|
+
activateModule.serverRoute,
|
|
528
|
+
deactivateModule.serverRoute,
|
|
412
529
|
overview.serverRoute,
|
|
413
530
|
settingsList.serverRoute,
|
|
414
531
|
settingsUpdate.serverRoute,
|
|
@@ -417,6 +534,9 @@ export function createSystemRoutes(options: SystemRouteOptions) {
|
|
|
417
534
|
|
|
418
535
|
export const endpoints = [
|
|
419
536
|
'system.modules.list',
|
|
537
|
+
'system.modules.active',
|
|
538
|
+
'system.modules.activate',
|
|
539
|
+
'system.modules.deactivate',
|
|
420
540
|
'system.overview.read',
|
|
421
541
|
'system.settings.list',
|
|
422
542
|
'system.settings.update',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { createSystemRoutes, endpoints } from './endpoints.ts';
|
|
2
2
|
export type {
|
|
3
|
+
ModuleCatalogPayload,
|
|
3
4
|
OverviewActivityPoint,
|
|
4
5
|
OverviewModulePoint,
|
|
5
6
|
SettingsEntryPayload,
|
|
@@ -9,3 +10,28 @@ export type {
|
|
|
9
10
|
} from './endpoints.ts';
|
|
10
11
|
export { readModuleCatalog } from './module-catalog.ts';
|
|
11
12
|
export type { ModuleCatalogEntry } from './module-catalog.ts';
|
|
13
|
+
export type { SystemModulesCapability } from './capability.ts';
|
|
14
|
+
export { createSystemRuntime } from './runtime.ts';
|
|
15
|
+
export type { SystemRuntime, SystemRuntimeOptions } from './runtime.ts';
|
|
16
|
+
export {
|
|
17
|
+
DEFAULT_SNAPSHOT_TENANTS,
|
|
18
|
+
DEFAULT_SNAPSHOT_TTL_MS,
|
|
19
|
+
ModuleActivationError,
|
|
20
|
+
ModuleActivationService,
|
|
21
|
+
} from '../services/module-activation-service.ts';
|
|
22
|
+
export type {
|
|
23
|
+
ModuleActivationErrorCode,
|
|
24
|
+
ModuleActivationServiceOptions,
|
|
25
|
+
} from '../services/module-activation-service.ts';
|
|
26
|
+
export {
|
|
27
|
+
DatabaseModuleActivationRepository,
|
|
28
|
+
migrateSystemDatabase,
|
|
29
|
+
} from '../services/database-repository.ts';
|
|
30
|
+
export {
|
|
31
|
+
databaseMigrations,
|
|
32
|
+
SYSTEM_TENANT_TABLES,
|
|
33
|
+
} from '../services/migration.ts';
|
|
34
|
+
export type {
|
|
35
|
+
ModuleActivationRecord,
|
|
36
|
+
ModuleActivationRepository,
|
|
37
|
+
} from '../services/repository.ts';
|
|
@@ -16,6 +16,11 @@ export interface ModuleCatalogEntry {
|
|
|
16
16
|
readonly platform: { readonly server: boolean; readonly client: boolean };
|
|
17
17
|
readonly enabled: boolean;
|
|
18
18
|
readonly directory: string;
|
|
19
|
+
/** Declared module dependencies, by id. */
|
|
20
|
+
readonly dependencies: readonly string[];
|
|
21
|
+
readonly provides: readonly string[];
|
|
22
|
+
/** Capability ids the module requires, optional requirements excluded. */
|
|
23
|
+
readonly requires: readonly string[];
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
interface Manifest {
|
|
@@ -23,6 +28,9 @@ interface Manifest {
|
|
|
23
28
|
version?: unknown;
|
|
24
29
|
capabilities?: unknown;
|
|
25
30
|
platform?: { server?: unknown; client?: unknown };
|
|
31
|
+
dependencies?: { id?: unknown }[];
|
|
32
|
+
provides?: unknown;
|
|
33
|
+
requires?: { id?: unknown; optional?: unknown }[];
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
interface Spec {
|
|
@@ -111,6 +119,17 @@ export function readModuleCatalog(
|
|
|
111
119
|
},
|
|
112
120
|
enabled: enabled.has(manifest.id),
|
|
113
121
|
directory,
|
|
122
|
+
dependencies: strings(
|
|
123
|
+
(Array.isArray(manifest.dependencies) ? manifest.dependencies : []).map(
|
|
124
|
+
(dependency) => dependency?.id,
|
|
125
|
+
),
|
|
126
|
+
),
|
|
127
|
+
provides: strings(manifest.provides),
|
|
128
|
+
requires: strings(
|
|
129
|
+
(Array.isArray(manifest.requires) ? manifest.requires : [])
|
|
130
|
+
.filter((requirement) => requirement?.optional !== true)
|
|
131
|
+
.map((requirement) => requirement?.id),
|
|
132
|
+
),
|
|
114
133
|
});
|
|
115
134
|
}
|
|
116
135
|
return entries;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DatabaseAdapterLease,
|
|
3
|
+
DatabaseProvider,
|
|
4
|
+
DatabaseProviderRequest,
|
|
5
|
+
} from '@flowdular/sdk/database';
|
|
6
|
+
import {
|
|
7
|
+
DATABASE_CAPABILITY_IDS,
|
|
8
|
+
DATABASE_DIALECT_IDS,
|
|
9
|
+
} from '@flowdular/sdk/database';
|
|
10
|
+
import {
|
|
11
|
+
DatabaseModuleActivationRepository,
|
|
12
|
+
migrateSystemDatabase,
|
|
13
|
+
} from '../services/database-repository.ts';
|
|
14
|
+
import {
|
|
15
|
+
ModuleActivationService,
|
|
16
|
+
type ModuleActivationServiceOptions,
|
|
17
|
+
} from '../services/module-activation-service.ts';
|
|
18
|
+
import type { ModuleActivationRepository } from '../services/repository.ts';
|
|
19
|
+
|
|
20
|
+
export interface SystemRuntimeOptions
|
|
21
|
+
extends Omit<ModuleActivationServiceOptions, 'repository'> {
|
|
22
|
+
/** Platform-owned provider. Modules never receive a DSN or a pool. */
|
|
23
|
+
readonly databases: DatabaseProvider;
|
|
24
|
+
readonly purpose: Exclude<DatabaseProviderRequest['purpose'], 'migration'>;
|
|
25
|
+
/** Supplied by tests that own the lease themselves. */
|
|
26
|
+
readonly repository?: ModuleActivationRepository;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SystemRuntime {
|
|
30
|
+
service(): Promise<ModuleActivationService>;
|
|
31
|
+
dispose(): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createSystemRuntime(
|
|
35
|
+
options: SystemRuntimeOptions,
|
|
36
|
+
): SystemRuntime {
|
|
37
|
+
let disposed = false;
|
|
38
|
+
let runtimeLeasePromise: Promise<DatabaseAdapterLease> | undefined;
|
|
39
|
+
let servicePromise: Promise<ModuleActivationService> | undefined;
|
|
40
|
+
const { databases, purpose, repository, ...serviceOptions } = options;
|
|
41
|
+
|
|
42
|
+
const build = (store: ModuleActivationRepository): ModuleActivationService =>
|
|
43
|
+
new ModuleActivationService({ ...serviceOptions, repository: store });
|
|
44
|
+
|
|
45
|
+
/* Schema work runs on the migrator role and that lease is released before the
|
|
46
|
+
runtime one is taken, so request handling never holds a schema owner. */
|
|
47
|
+
const initialize = async (): Promise<ModuleActivationService> => {
|
|
48
|
+
if (repository) return build(repository);
|
|
49
|
+
const migrationLease = await databases.acquire({
|
|
50
|
+
namespace: 'system.core',
|
|
51
|
+
purpose: 'migration',
|
|
52
|
+
requirements: {
|
|
53
|
+
dialectIds: [DATABASE_DIALECT_IDS.postgresql],
|
|
54
|
+
capabilities: [
|
|
55
|
+
DATABASE_CAPABILITY_IDS.MIGRATION_LOCK,
|
|
56
|
+
DATABASE_CAPABILITY_IDS.SCHEMA_INTROSPECTION,
|
|
57
|
+
DATABASE_CAPABILITY_IDS.TRANSACTIONAL_DDL,
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
try {
|
|
62
|
+
await migrateSystemDatabase(migrationLease.database);
|
|
63
|
+
} finally {
|
|
64
|
+
await migrationLease.release();
|
|
65
|
+
}
|
|
66
|
+
runtimeLeasePromise = databases.acquire({
|
|
67
|
+
namespace: 'system.core',
|
|
68
|
+
purpose,
|
|
69
|
+
requirements: {
|
|
70
|
+
dialectIds: [DATABASE_DIALECT_IDS.postgresql],
|
|
71
|
+
capabilities: [
|
|
72
|
+
DATABASE_CAPABILITY_IDS.TRANSACTIONS,
|
|
73
|
+
DATABASE_CAPABILITY_IDS.TENANT_CONTEXT,
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
const lease = await runtimeLeasePromise;
|
|
78
|
+
return build(new DatabaseModuleActivationRepository(lease.database));
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
service: () => {
|
|
83
|
+
if (disposed) {
|
|
84
|
+
return Promise.reject(new Error('System runtime is disposed.'));
|
|
85
|
+
}
|
|
86
|
+
servicePromise ??= initialize();
|
|
87
|
+
return servicePromise;
|
|
88
|
+
},
|
|
89
|
+
async dispose() {
|
|
90
|
+
if (disposed) return;
|
|
91
|
+
disposed = true;
|
|
92
|
+
if (!runtimeLeasePromise) {
|
|
93
|
+
await servicePromise?.catch(() => undefined);
|
|
94
|
+
}
|
|
95
|
+
if (!runtimeLeasePromise) return;
|
|
96
|
+
const lease = await runtimeLeasePromise;
|
|
97
|
+
await lease.release();
|
|
98
|
+
runtimeLeasePromise = undefined;
|
|
99
|
+
servicePromise = undefined;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { DatabaseHandle } from '@flowdular/sdk/database';
|
|
2
|
+
import { runDatabaseMigrations } from '@flowdular/sdk/database';
|
|
3
|
+
import { databaseMigrations } from './migration.ts';
|
|
4
|
+
import type {
|
|
5
|
+
ModuleActivationRecord,
|
|
6
|
+
ModuleActivationRepository,
|
|
7
|
+
} from './repository.ts';
|
|
8
|
+
|
|
9
|
+
interface ActivationRow {
|
|
10
|
+
tenant_id: string;
|
|
11
|
+
module_id: string;
|
|
12
|
+
active: number | bigint | string;
|
|
13
|
+
changed_by: string;
|
|
14
|
+
changed_at: number | bigint | string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const SQL = {
|
|
18
|
+
list: `SELECT tenant_id, module_id, active, changed_by, changed_at
|
|
19
|
+
FROM system_module_activations
|
|
20
|
+
WHERE tenant_id = $1
|
|
21
|
+
ORDER BY module_id`,
|
|
22
|
+
set: `INSERT INTO system_module_activations
|
|
23
|
+
(tenant_id, module_id, active, changed_by, changed_at)
|
|
24
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
25
|
+
ON CONFLICT (tenant_id, module_id) DO UPDATE
|
|
26
|
+
SET active = EXCLUDED.active,
|
|
27
|
+
changed_by = EXCLUDED.changed_by,
|
|
28
|
+
changed_at = EXCLUDED.changed_at
|
|
29
|
+
WHERE system_module_activations.tenant_id = $1`,
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
/* PostgreSQL returns BIGINT as a string, and a flag column as a number this
|
|
33
|
+
module compares only after normalizing. */
|
|
34
|
+
function integer(value: number | bigint | string, field: string): number {
|
|
35
|
+
const normalized = Number(value);
|
|
36
|
+
if (!Number.isSafeInteger(normalized) || normalized < 0) {
|
|
37
|
+
throw new Error(`The system database returned an invalid ${field}.`);
|
|
38
|
+
}
|
|
39
|
+
return normalized;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function fromRow(row: ActivationRow): ModuleActivationRecord {
|
|
43
|
+
return {
|
|
44
|
+
tenantId: row.tenant_id,
|
|
45
|
+
moduleId: row.module_id,
|
|
46
|
+
active: integer(row.active, 'flag') === 1,
|
|
47
|
+
changedBy: row.changed_by,
|
|
48
|
+
changedAt: integer(row.changed_at, 'timestamp'),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class DatabaseModuleActivationRepository
|
|
53
|
+
implements ModuleActivationRepository
|
|
54
|
+
{
|
|
55
|
+
constructor(private readonly database: DatabaseHandle) {}
|
|
56
|
+
|
|
57
|
+
async list(tenantId: string): Promise<readonly ModuleActivationRecord[]> {
|
|
58
|
+
const result = await this.database.transaction(
|
|
59
|
+
(transaction) =>
|
|
60
|
+
transaction.query<ActivationRow>({
|
|
61
|
+
text: SQL.list,
|
|
62
|
+
parameters: [tenantId],
|
|
63
|
+
}),
|
|
64
|
+
{ access: 'read', tenantId },
|
|
65
|
+
);
|
|
66
|
+
return result.rows.map(fromRow);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async set(record: ModuleActivationRecord): Promise<void> {
|
|
70
|
+
await this.database.transaction(
|
|
71
|
+
(transaction) =>
|
|
72
|
+
transaction.execute({
|
|
73
|
+
text: SQL.set,
|
|
74
|
+
parameters: [
|
|
75
|
+
record.tenantId,
|
|
76
|
+
record.moduleId,
|
|
77
|
+
record.active ? 1 : 0,
|
|
78
|
+
record.changedBy,
|
|
79
|
+
record.changedAt,
|
|
80
|
+
],
|
|
81
|
+
}),
|
|
82
|
+
{ access: 'write', tenantId: record.tenantId },
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function migrateSystemDatabase(
|
|
88
|
+
database: DatabaseHandle,
|
|
89
|
+
): Promise<void> {
|
|
90
|
+
await runDatabaseMigrations(database, 'system.core', databaseMigrations);
|
|
91
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { DatabaseMigration } from '@flowdular/sdk/database';
|
|
2
|
+
import { postgresTenantTableState } from '@flowdular/sdk/database';
|
|
3
|
+
|
|
4
|
+
/* Mirrors migrations/0001_system_module_activations.up.sql byte for byte;
|
|
5
|
+
tests/migrations.test.ts fails on drift. */
|
|
6
|
+
export const SYSTEM_MIGRATION_001 = `-- One row per module whose activation an owner changed for a workspace. A
|
|
7
|
+
-- composed module without a row is active; the row keeps the last decision and
|
|
8
|
+
-- who made it. The primary key is the only lookup: the snapshot reads every row
|
|
9
|
+
-- of one tenant at once.
|
|
10
|
+
CREATE TABLE IF NOT EXISTS system_module_activations (
|
|
11
|
+
tenant_id TEXT NOT NULL,
|
|
12
|
+
module_id TEXT NOT NULL CHECK (length(module_id) BETWEEN 1 AND 128),
|
|
13
|
+
active SMALLINT NOT NULL CHECK (active IN (0, 1)),
|
|
14
|
+
changed_by TEXT NOT NULL CHECK (length(changed_by) BETWEEN 1 AND 64),
|
|
15
|
+
changed_at BIGINT NOT NULL,
|
|
16
|
+
PRIMARY KEY (tenant_id, module_id)
|
|
17
|
+
);
|
|
18
|
+
ALTER TABLE system_module_activations ENABLE ROW LEVEL SECURITY;
|
|
19
|
+
ALTER TABLE system_module_activations FORCE ROW LEVEL SECURITY;
|
|
20
|
+
CREATE POLICY system_module_activations_tenant_policy ON system_module_activations
|
|
21
|
+
USING (tenant_id = current_setting('coreloom.tenant_id', true))
|
|
22
|
+
WITH CHECK (tenant_id = current_setting('coreloom.tenant_id', true));
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
/** Every tenant table of this module, with the policy each one must carry. */
|
|
26
|
+
export const SYSTEM_TENANT_TABLES = ['system_module_activations'] as const;
|
|
27
|
+
|
|
28
|
+
export const databaseMigrations: readonly DatabaseMigration[] = [
|
|
29
|
+
{
|
|
30
|
+
id: '0001_system_module_activations',
|
|
31
|
+
sql: { postgresql: SYSTEM_MIGRATION_001 },
|
|
32
|
+
inspectExisting: (database) =>
|
|
33
|
+
postgresTenantTableState(
|
|
34
|
+
database,
|
|
35
|
+
'system_module_activations',
|
|
36
|
+
'system_module_activations_tenant_policy',
|
|
37
|
+
),
|
|
38
|
+
},
|
|
39
|
+
];
|