@oktis-works/core 0.5.15 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cache/tags.d.ts +12 -0
- package/dist/cache/tags.d.ts.map +1 -0
- package/dist/cache/tags.js +55 -0
- package/dist/cache/tags.js.map +1 -0
- package/dist/comments/service.d.ts +54 -0
- package/dist/comments/service.d.ts.map +1 -0
- package/dist/comments/service.js +166 -0
- package/dist/comments/service.js.map +1 -0
- package/dist/content/service.d.ts.map +1 -1
- package/dist/content/service.js +24 -14
- package/dist/content/service.js.map +1 -1
- package/dist/fields/location-rules.d.ts +3 -1
- package/dist/fields/location-rules.d.ts.map +1 -1
- package/dist/fields/location-rules.js +1 -0
- package/dist/fields/location-rules.js.map +1 -1
- package/dist/fields/service.d.ts +41 -0
- package/dist/fields/service.d.ts.map +1 -1
- package/dist/fields/service.js +134 -0
- package/dist/fields/service.js.map +1 -1
- package/dist/fields/types/registry.d.ts +18 -0
- package/dist/fields/types/registry.d.ts.map +1 -1
- package/dist/fields/types/registry.js +13 -0
- package/dist/fields/types/registry.js.map +1 -1
- package/dist/hooks/points.d.ts +6 -0
- package/dist/hooks/points.d.ts.map +1 -1
- package/dist/hooks/points.js +7 -0
- package/dist/hooks/points.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/mail/service.d.ts +26 -0
- package/dist/mail/service.d.ts.map +1 -0
- package/dist/mail/service.js +117 -0
- package/dist/mail/service.js.map +1 -0
- package/dist/plugin/load.d.ts +80 -13
- package/dist/plugin/load.d.ts.map +1 -1
- package/dist/plugin/load.js +315 -50
- package/dist/plugin/load.js.map +1 -1
- package/dist/plugin/service.d.ts.map +1 -1
- package/dist/plugin/service.js +5 -1
- package/dist/plugin/service.js.map +1 -1
- package/dist/queue/producer.d.ts +12 -1
- package/dist/queue/producer.d.ts.map +1 -1
- package/dist/queue/producer.js +92 -0
- package/dist/queue/producer.js.map +1 -1
- package/dist/role/plugin-roles.d.ts +39 -0
- package/dist/role/plugin-roles.d.ts.map +1 -0
- package/dist/role/plugin-roles.js +161 -0
- package/dist/role/plugin-roles.js.map +1 -0
- package/dist/webhooks/service.d.ts +33 -0
- package/dist/webhooks/service.d.ts.map +1 -0
- package/dist/webhooks/service.js +57 -0
- package/dist/webhooks/service.js.map +1 -0
- package/package.json +6 -6
package/dist/plugin/load.d.ts
CHANGED
|
@@ -1,5 +1,70 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
1
|
+
import type { RouteDefinition, PluginBootstrapResult } from '@oktis-works/plugin-runtime';
|
|
2
|
+
import type { AdminExtensionsState, PluginBlockManifest, PluginPatternManifest } from '@oktis-works/types';
|
|
3
|
+
import type { EnqueuedAsset, PluginHealthCheck } from '@oktis-works/plugin-sdk';
|
|
4
|
+
/** Plugin job handler (dispatched by the worker). */
|
|
5
|
+
export type PluginJobHandlerFn = (payload: unknown, context: {
|
|
6
|
+
tenantId: string | null;
|
|
7
|
+
plugin: string;
|
|
8
|
+
jobName: string;
|
|
9
|
+
}) => void | Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Resolves the handler registered by a plugin for a job (via api.jobs.on).
|
|
12
|
+
* Scope: platform (`tenantId` null) or tenant (loaded on demand).
|
|
13
|
+
* Never throws — null when the plugin is disabled or the handler is absent.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getPluginJobHandler(tenantId: string | null, pluginName: string, jobName: string): Promise<PluginJobHandlerFn | null>;
|
|
16
|
+
/**
|
|
17
|
+
* `api.admin` slots loaded in a scope (platform `null` or `tenant:<id>`) —
|
|
18
|
+
* feeds GET /api/v1/plugins/extensions together with `manifest.admin`.
|
|
19
|
+
* Never throws: runtime unavailability becomes an empty list.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getPluginAdminExtensions(tenantId: string | null): Promise<Array<{
|
|
22
|
+
pluginName: string;
|
|
23
|
+
state: AdminExtensionsState;
|
|
24
|
+
}>>;
|
|
25
|
+
/**
|
|
26
|
+
* Blocks registered at runtime by the loaded plugins of a scope — feeds
|
|
27
|
+
* GET /api/v1/blocks together with `manifest.blocks`. Never throws: runtime
|
|
28
|
+
* unavailability becomes an empty list.
|
|
29
|
+
*/
|
|
30
|
+
export declare function getPluginBlocks(tenantId: string | null): Promise<Array<{
|
|
31
|
+
pluginName: string;
|
|
32
|
+
blocks: Array<PluginBlockManifest & {
|
|
33
|
+
pluginName: string;
|
|
34
|
+
}>;
|
|
35
|
+
}>>;
|
|
36
|
+
/**
|
|
37
|
+
* Patterns registered at runtime by the loaded plugins of a scope — feeds
|
|
38
|
+
* GET /api/v1/patterns together with `manifest.patterns`. Never throws:
|
|
39
|
+
* runtime unavailability becomes an empty list.
|
|
40
|
+
*/
|
|
41
|
+
export declare function getPluginPatterns(tenantId: string | null): Promise<Array<{
|
|
42
|
+
pluginName: string;
|
|
43
|
+
patterns: Array<PluginPatternManifest & {
|
|
44
|
+
pluginName: string;
|
|
45
|
+
}>;
|
|
46
|
+
}>>;
|
|
47
|
+
/**
|
|
48
|
+
* Assets enqueued by the loaded plugins of a scope — feeds the public
|
|
49
|
+
* renderer's theme:head pipeline (Item 8). Never throws: runtime
|
|
50
|
+
* unavailability becomes an empty list.
|
|
51
|
+
*/
|
|
52
|
+
export declare function getPluginAssets(tenantId: string | null): Promise<Array<{
|
|
53
|
+
pluginName: string;
|
|
54
|
+
assets: EnqueuedAsset[];
|
|
55
|
+
}>>;
|
|
56
|
+
/**
|
|
57
|
+
* Health checks registered by the loaded plugins of a scope — aggregated by
|
|
58
|
+
* GET /api/v1/health (Item 14). Never throws: runtime unavailability becomes
|
|
59
|
+
* an empty list.
|
|
60
|
+
*/
|
|
61
|
+
export declare function getPluginHealthChecks(tenantId: string | null): Promise<Array<{
|
|
62
|
+
pluginName: string;
|
|
63
|
+
checks: Array<{
|
|
64
|
+
name: string;
|
|
65
|
+
check: PluginHealthCheck;
|
|
66
|
+
}>;
|
|
67
|
+
}>>;
|
|
3
68
|
export interface PluginsScopeState {
|
|
4
69
|
/** `platform` ou `tenant:<id>`. */
|
|
5
70
|
scope: string;
|
|
@@ -21,36 +86,38 @@ export interface PluginsRuntimeState {
|
|
|
21
86
|
export interface PluginRouteEntry {
|
|
22
87
|
pluginName: string;
|
|
23
88
|
routes: RouteDefinition[];
|
|
89
|
+
/** rpm do rate limit por plugin (manifest rateLimit.rpm, Item 17). */
|
|
90
|
+
rpm?: number;
|
|
24
91
|
}
|
|
25
92
|
/** Registra rotas de um plugin (chamado durante bootstrap). */
|
|
26
|
-
export declare function registerPluginRoutes(pluginName: string, routes: RouteDefinition[]): void;
|
|
93
|
+
export declare function registerPluginRoutes(pluginName: string, routes: RouteDefinition[], rpm?: number): void;
|
|
27
94
|
/** Remove rotas de um plugin (quando desativado/desinstalado). */
|
|
28
95
|
export declare function unregisterPluginRoutes(pluginName: string): boolean;
|
|
29
|
-
/**
|
|
96
|
+
/** Gets every route registered for mounting on the API router. */
|
|
30
97
|
export declare function getRegisteredPluginRoutes(): PluginRouteEntry[];
|
|
31
98
|
/**
|
|
32
99
|
* Sincroniza o registry **platform** com os plugins habilitados no banco.
|
|
33
|
-
*
|
|
34
|
-
*
|
|
100
|
+
* Implicit force: this is the explicit boot/activation call — reflects the
|
|
101
|
+
* database immediately. Idempotent and never throws.
|
|
35
102
|
*/
|
|
36
103
|
export declare function syncEnabledPlugins(options?: {
|
|
37
104
|
force?: boolean;
|
|
38
105
|
}): Promise<PluginBootstrapResult>;
|
|
39
106
|
/**
|
|
40
|
-
*
|
|
107
|
+
* Ensures the tenant's plugins are loaded into its own registry.
|
|
41
108
|
* Chamado sob demanda (middleware/API, web, worker) — com cache de TTL para
|
|
42
|
-
*
|
|
109
|
+
* avoids hitting the database on every request. Never throws.
|
|
43
110
|
*/
|
|
44
111
|
export declare function ensureTenantPluginsLoaded(tenantId: string, options?: {
|
|
45
112
|
force?: boolean;
|
|
46
113
|
}): Promise<PluginBootstrapResult>;
|
|
47
114
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
115
|
+
* Invalidates a tenant's cache/registry (after enabling/disabling a link,
|
|
116
|
+
* uninstalling or changing permissions). The tenant's next request reloads.
|
|
50
117
|
*/
|
|
51
|
-
export declare function invalidateTenantPlugins(tenantId: string): void
|
|
52
|
-
/**
|
|
118
|
+
export declare function invalidateTenantPlugins(tenantId: string): Promise<void>;
|
|
119
|
+
/** Invalidates the platform scope (after global changes). */
|
|
53
120
|
export declare function invalidatePlatformPlugins(): void;
|
|
54
|
-
/**
|
|
121
|
+
/** Per-scope load state — observability surface (WP2). */
|
|
55
122
|
export declare function getPluginsRuntimeState(): PluginsRuntimeState;
|
|
56
123
|
//# sourceMappingURL=load.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../../src/plugin/load.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../../src/plugin/load.ts"],"names":[],"mappings":"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"}
|
package/dist/plugin/load.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
// @oktis-works/core -
|
|
1
|
+
// @oktis-works/core - Enabled plugins runtime sync
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// PLUGIN.md promises that `register(registry)` is called when the plugin is
|
|
4
|
+
// loaded — the core honors it for every process (API, web, worker, dev). Two
|
|
5
|
+
// scopes:
|
|
6
6
|
//
|
|
7
|
-
// - **platform** (`syncEnabledPlugins`): plugins `tenant_id IS NULL`,
|
|
8
|
-
//
|
|
9
|
-
// - **tenant** (`ensureTenantPluginsLoaded`): plugins
|
|
10
|
-
// `plugin_tenants` (status
|
|
11
|
-
//
|
|
7
|
+
// - **platform** (`syncEnabledPlugins`): plugins with `tenant_id IS NULL`,
|
|
8
|
+
// enabled globally → platform registry, loaded at boot;
|
|
9
|
+
// - **tenant** (`ensureTenantPluginsLoaded`): plugins linked in
|
|
10
|
+
// `plugin_tenants` (ENABLED status) → tenant registry, loaded on demand
|
|
11
|
+
// at that tenant's first request (TTL + LRU cache).
|
|
12
12
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// boot.
|
|
16
|
-
// `/api/v1/plugins/runtime` + badge
|
|
17
|
-
import {
|
|
13
|
+
// Never throws: database failure, missing directory or a broken plugin are
|
|
14
|
+
// reported in the result (and persisted in `plugins.error`) without breaking
|
|
15
|
+
// the boot. Failures are visible in `getPluginsRuntimeState()` (the
|
|
16
|
+
// `/api/v1/plugins/runtime` route + admin badge) — never silenced.
|
|
17
|
+
import { createHash } from 'node:crypto';
|
|
18
18
|
import { getConnection } from '@oktis-works/database';
|
|
19
19
|
import { loadConfig } from '@oktis-works/config';
|
|
20
20
|
import { resolvePluginPermissionPolicy } from '@oktis-works/validation';
|
|
@@ -24,23 +24,143 @@ import { getEventBus } from '../events/bus.js';
|
|
|
24
24
|
import { tenantService } from '../tenant/service.js';
|
|
25
25
|
import { userService } from '../user/service.js';
|
|
26
26
|
import { fieldGroupService } from '../fields/service.js';
|
|
27
|
+
import { postTypeRegistry } from '../content/post-types.js';
|
|
28
|
+
import { taxonomyService } from '../taxonomies/service.js';
|
|
29
|
+
import { pluginRolesService } from '../role/plugin-roles.js';
|
|
30
|
+
import { mailService } from '../mail/service.js';
|
|
31
|
+
import { auditService } from '../observability/audit.js';
|
|
32
|
+
import { getCache } from '../cache/index.js';
|
|
33
|
+
import { queueProducer } from '../queue/producer.js';
|
|
27
34
|
import { getCurrentTenantIdSync, getCurrentUserIdSync, getCurrentRequestIdSync } from '../tenant/context.js';
|
|
35
|
+
// Dynamic imports for the circular dependency with plugin-runtime
|
|
36
|
+
// (non-literal require so TypeScript cannot resolve it at build time)
|
|
37
|
+
const getPluginRuntime = async () => {
|
|
38
|
+
return require('@oktis-works/plugin-runtime');
|
|
39
|
+
};
|
|
40
|
+
let _pluginRuntimeCache = null;
|
|
41
|
+
const getPluginRuntimeCached = async () => {
|
|
42
|
+
if (!_pluginRuntimeCache) {
|
|
43
|
+
_pluginRuntimeCache = await getPluginRuntime();
|
|
44
|
+
}
|
|
45
|
+
return _pluginRuntimeCache;
|
|
46
|
+
};
|
|
47
|
+
const getBootstrapPlugins = async () => (await getPluginRuntimeCached()).bootstrapPlugins;
|
|
48
|
+
const getHookRegistryStore = async () => (await getPluginRuntimeCached()).getHookRegistryStore;
|
|
49
|
+
const unloadTenant = async () => (await getPluginRuntimeCached()).unloadTenant;
|
|
50
|
+
const getLoadedPluginRoutes = async () => (await getPluginRuntimeCached()).getLoadedPluginRoutes;
|
|
51
|
+
const getLoadedPluginJobHandlers = async () => (await getPluginRuntimeCached()).getLoadedPluginJobHandlers;
|
|
52
|
+
const getLoadedPluginAdminExtensions = async () => (await getPluginRuntimeCached()).getLoadedPluginAdminExtensions;
|
|
53
|
+
const getLoadedPluginBlocks = async () => (await getPluginRuntimeCached()).getLoadedPluginBlocks;
|
|
54
|
+
const getLoadedPluginPatterns = async () => (await getPluginRuntimeCached()).getLoadedPluginPatterns;
|
|
55
|
+
const getLoadedPluginAssets = async () => (await getPluginRuntimeCached()).getLoadedPluginAssets;
|
|
56
|
+
const getLoadedPluginHealthChecks = async () => (await getPluginRuntimeCached()).getLoadedPluginHealthChecks;
|
|
57
|
+
/** Deterministic plugin job id (schedules are idempotent by it). */
|
|
58
|
+
function pluginJobId(tenantId, pluginName, jobName) {
|
|
59
|
+
return `plugin:${tenantId ?? 'platform'}:${pluginName}:${jobName}`;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Resolves the handler registered by a plugin for a job (via api.jobs.on).
|
|
63
|
+
* Scope: platform (`tenantId` null) or tenant (loaded on demand).
|
|
64
|
+
* Never throws — null when the plugin is disabled or the handler is absent.
|
|
65
|
+
*/
|
|
66
|
+
export async function getPluginJobHandler(tenantId, pluginName, jobName) {
|
|
67
|
+
try {
|
|
68
|
+
const entries = await (await getLoadedPluginJobHandlers())(scopeKey(tenantId));
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
if (entry.pluginName === pluginName) {
|
|
71
|
+
return entry.handlers.get(jobName) ?? null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* `api.admin` slots loaded in a scope (platform `null` or `tenant:<id>`) —
|
|
82
|
+
* feeds GET /api/v1/plugins/extensions together with `manifest.admin`.
|
|
83
|
+
* Never throws: runtime unavailability becomes an empty list.
|
|
84
|
+
*/
|
|
85
|
+
export async function getPluginAdminExtensions(tenantId) {
|
|
86
|
+
try {
|
|
87
|
+
return await (await getLoadedPluginAdminExtensions())(scopeKey(tenantId));
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Blocks registered at runtime by the loaded plugins of a scope — feeds
|
|
95
|
+
* GET /api/v1/blocks together with `manifest.blocks`. Never throws: runtime
|
|
96
|
+
* unavailability becomes an empty list.
|
|
97
|
+
*/
|
|
98
|
+
export async function getPluginBlocks(tenantId) {
|
|
99
|
+
try {
|
|
100
|
+
return await (await getLoadedPluginBlocks())(scopeKey(tenantId));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Patterns registered at runtime by the loaded plugins of a scope — feeds
|
|
108
|
+
* GET /api/v1/patterns together with `manifest.patterns`. Never throws:
|
|
109
|
+
* runtime unavailability becomes an empty list.
|
|
110
|
+
*/
|
|
111
|
+
export async function getPluginPatterns(tenantId) {
|
|
112
|
+
try {
|
|
113
|
+
return await (await getLoadedPluginPatterns())(scopeKey(tenantId));
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Assets enqueued by the loaded plugins of a scope — feeds the public
|
|
121
|
+
* renderer's theme:head pipeline (Item 8). Never throws: runtime
|
|
122
|
+
* unavailability becomes an empty list.
|
|
123
|
+
*/
|
|
124
|
+
export async function getPluginAssets(tenantId) {
|
|
125
|
+
try {
|
|
126
|
+
return await (await getLoadedPluginAssets())(scopeKey(tenantId));
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Health checks registered by the loaded plugins of a scope — aggregated by
|
|
134
|
+
* GET /api/v1/health (Item 14). Never throws: runtime unavailability becomes
|
|
135
|
+
* an empty list.
|
|
136
|
+
*/
|
|
137
|
+
export async function getPluginHealthChecks(tenantId) {
|
|
138
|
+
try {
|
|
139
|
+
return await (await getLoadedPluginHealthChecks())(scopeKey(tenantId));
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
28
145
|
const pluginRoutesRegistry = new Map();
|
|
29
146
|
/** Registra rotas de um plugin (chamado durante bootstrap). */
|
|
30
|
-
export function registerPluginRoutes(pluginName, routes) {
|
|
147
|
+
export function registerPluginRoutes(pluginName, routes, rpm) {
|
|
31
148
|
if (routes.length === 0)
|
|
32
149
|
return;
|
|
33
|
-
pluginRoutesRegistry.set(pluginName, { pluginName, routes });
|
|
150
|
+
pluginRoutesRegistry.set(pluginName, { pluginName, routes, ...(rpm ? { rpm } : {}) });
|
|
34
151
|
}
|
|
35
152
|
/** Remove rotas de um plugin (quando desativado/desinstalado). */
|
|
36
153
|
export function unregisterPluginRoutes(pluginName) {
|
|
37
154
|
return pluginRoutesRegistry.delete(pluginName);
|
|
38
155
|
}
|
|
39
|
-
/**
|
|
156
|
+
/** Gets every route registered for mounting on the API router. */
|
|
40
157
|
export function getRegisteredPluginRoutes() {
|
|
41
158
|
return [...pluginRoutesRegistry.values()];
|
|
42
159
|
}
|
|
43
|
-
/**
|
|
160
|
+
/** Item 21: registro das consultas preparadas por hash (teto LRU). */
|
|
161
|
+
const PREPARED_CACHE_MAX = 200;
|
|
162
|
+
const preparedPluginStatements = new Map();
|
|
163
|
+
/** Per-scope states — iteration order = LRU. */
|
|
44
164
|
const states = new Map();
|
|
45
165
|
const inflight = new Map();
|
|
46
166
|
const PLATFORM_KEY = 'platform';
|
|
@@ -63,9 +183,31 @@ function createApiDeps(tenantId) {
|
|
|
63
183
|
db: {
|
|
64
184
|
query: async (sql, params) => {
|
|
65
185
|
const conn = getConnection();
|
|
186
|
+
// Item 21: registro de prepared statements por hash — dedupe + LRU +
|
|
187
|
+
// contagem de uso por consulta (identidade da consulta preparada em
|
|
188
|
+
// api.query; o driver reutiliza o plano para consultas repetidas).
|
|
189
|
+
const hash = createHash('sha256').update(sql).digest('hex').slice(0, 24);
|
|
190
|
+
let entry = preparedPluginStatements.get(hash);
|
|
191
|
+
if (!entry) {
|
|
192
|
+
entry = { sql, uses: 0 };
|
|
193
|
+
preparedPluginStatements.set(hash, entry);
|
|
194
|
+
if (preparedPluginStatements.size > PREPARED_CACHE_MAX) {
|
|
195
|
+
const oldest = preparedPluginStatements.keys().next().value;
|
|
196
|
+
if (oldest)
|
|
197
|
+
preparedPluginStatements.delete(oldest);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
entry.uses += 1;
|
|
66
201
|
return conn.unsafe(sql, params ?? []);
|
|
67
202
|
},
|
|
68
203
|
},
|
|
204
|
+
// api.cache backend (Item 2) — lazy so contexts that never use the
|
|
205
|
+
// plugin cache don't need it at boot.
|
|
206
|
+
cache: {
|
|
207
|
+
get: (key) => getCache().get(key),
|
|
208
|
+
set: (key, value, ttlSeconds) => getCache().set(key, value, ttlSeconds),
|
|
209
|
+
del: (key) => getCache().del(key),
|
|
210
|
+
},
|
|
69
211
|
getTenantId: () => tenantId ?? getCurrentTenantIdSync(),
|
|
70
212
|
getUserId: () => getCurrentUserIdSync(),
|
|
71
213
|
getRequestId: () => getCurrentRequestIdSync(),
|
|
@@ -78,14 +220,14 @@ function createApiDeps(tenantId) {
|
|
|
78
220
|
return user ? { id: user.id, email: user.email, name: user.name, roles: user.roles } : null;
|
|
79
221
|
},
|
|
80
222
|
registerRoutes: (routes) => {
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
console.debug(`[PluginAPI] ${routes.length}
|
|
223
|
+
// Routes registered here are collected at bootstrap by the PluginAPI
|
|
224
|
+
// itself (getLoadedPluginRoutes) — this point only logs.
|
|
225
|
+
console.debug(`[PluginAPI] ${routes.length} route(s) registered — collected at bootstrap`);
|
|
84
226
|
},
|
|
85
227
|
registerContentFields: async (fields) => {
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
228
|
+
// Explicit guard: with noUncheckedIndexedAccess, fields[0] is
|
|
229
|
+
// T | undefined even with the length check above — checked
|
|
230
|
+
// destructuring leaves the narrowing to TS with the same semantics.
|
|
89
231
|
const firstField = fields[0];
|
|
90
232
|
if (!firstField)
|
|
91
233
|
return;
|
|
@@ -116,12 +258,135 @@ function createApiDeps(tenantId) {
|
|
|
116
258
|
fields: fieldInputs,
|
|
117
259
|
});
|
|
118
260
|
}
|
|
119
|
-
console.debug(`[PluginAPI] ${fields.length}
|
|
261
|
+
console.debug(`[PluginAPI] ${fields.length} content field(s) registered in field group "${groupKey}"`);
|
|
120
262
|
}
|
|
121
263
|
catch (error) {
|
|
122
|
-
console.error('[PluginAPI]
|
|
264
|
+
console.error('[PluginAPI] Failed to register content fields:', error);
|
|
123
265
|
}
|
|
124
266
|
},
|
|
267
|
+
registerFieldTypes: async (fieldTypes) => {
|
|
268
|
+
try {
|
|
269
|
+
// Same package — relative import (a self-import of '@oktis-works/core'
|
|
270
|
+
// does not resolve on a fresh checkout, where dist/ does not exist yet).
|
|
271
|
+
const { fieldTypeExtensionService } = await import('../fields/service.js');
|
|
272
|
+
for (const ft of fieldTypes) {
|
|
273
|
+
await fieldTypeExtensionService.register(ft);
|
|
274
|
+
}
|
|
275
|
+
await fieldTypeExtensionService.syncToRegistry();
|
|
276
|
+
console.debug(`[PluginAPI] ${fieldTypes.length} tipo(s) de campo registrado(s) no registry`);
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
console.error('[PluginAPI] Falha ao registrar tipos de campo:', error);
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
registerPostTypes: async (postTypes) => {
|
|
283
|
+
// Idempotent persistence for plugin Custom Post Types (item 1 of the
|
|
284
|
+
// plugin-parity-plan): existe com mesmo sourceId → atualiza; existe com
|
|
285
|
+
// outra origem (CORE/ADMIN/outro plugin) → warn + skip (nunca rouba).
|
|
286
|
+
try {
|
|
287
|
+
for (const type of postTypes) {
|
|
288
|
+
const supports = ['title', 'editor'];
|
|
289
|
+
if (type.revisions)
|
|
290
|
+
supports.push('revisions');
|
|
291
|
+
const existing = await postTypeRegistry.getBySlug(type.key);
|
|
292
|
+
if (existing) {
|
|
293
|
+
if (existing.source === 'PLUGIN' && existing.sourceId === type.pluginName) {
|
|
294
|
+
await postTypeRegistry.update(type.key, {
|
|
295
|
+
singularLabel: type.label,
|
|
296
|
+
pluralLabel: type.plural ?? existing.pluralLabel,
|
|
297
|
+
menuIcon: type.icon ?? existing.menuIcon,
|
|
298
|
+
supports,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
console.warn(`[PluginAPI] Post type "${type.key}" already exists (${existing.source}) — "${type.pluginName}" cannot claim it`);
|
|
303
|
+
}
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
await postTypeRegistry.create({
|
|
307
|
+
name: type.key,
|
|
308
|
+
slug: type.key,
|
|
309
|
+
pluralLabel: type.plural ?? `${type.label}s`,
|
|
310
|
+
singularLabel: type.label,
|
|
311
|
+
source: 'PLUGIN',
|
|
312
|
+
sourceId: type.pluginName,
|
|
313
|
+
supports,
|
|
314
|
+
hasArchive: true,
|
|
315
|
+
menuIcon: type.icon ?? 'box',
|
|
316
|
+
singleton: false,
|
|
317
|
+
public: true,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
console.debug(`[PluginAPI] ${postTypes.length} post type(s) de plugin persistido(s)`);
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
console.error('[PluginAPI] Falha ao registrar post types:', error);
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
registerTaxonomies: async (taxonomies) => {
|
|
327
|
+
// Idempotent persistence for plugin Custom Taxonomies — ownership
|
|
328
|
+
// stored in meta.plugin (schema has no sourceId for taxonomies).
|
|
329
|
+
try {
|
|
330
|
+
for (const taxonomy of taxonomies) {
|
|
331
|
+
const existing = await taxonomyService.getBySlug(taxonomy.key);
|
|
332
|
+
if (existing) {
|
|
333
|
+
const owner = existing.meta?.['plugin'];
|
|
334
|
+
if (owner !== undefined && owner !== taxonomy.pluginName) {
|
|
335
|
+
console.warn(`[PluginAPI] Taxonomy "${taxonomy.key}" belongs to plugin "${String(owner)}" — "${taxonomy.pluginName}" cannot claim it`);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
await taxonomyService.update(taxonomy.key, {
|
|
339
|
+
name: taxonomy.label,
|
|
340
|
+
hierarchical: taxonomy.hierarchical ?? false,
|
|
341
|
+
attachTo: taxonomy.postTypes,
|
|
342
|
+
meta: { ...(existing.meta ?? {}), plugin: taxonomy.pluginName },
|
|
343
|
+
});
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
await taxonomyService.create({
|
|
347
|
+
name: taxonomy.label,
|
|
348
|
+
slug: taxonomy.key,
|
|
349
|
+
hierarchical: taxonomy.hierarchical ?? false,
|
|
350
|
+
attachTo: taxonomy.postTypes,
|
|
351
|
+
meta: { plugin: taxonomy.pluginName },
|
|
352
|
+
source: 'PLUGIN',
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
console.debug(`[PluginAPI] ${taxonomies.length} taxonomia(s) de plugin persistida(s)`);
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
console.error('[PluginAPI] Falha ao registrar taxonomias:', error);
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
jobs: {
|
|
362
|
+
schedule: async (jobName, spec, ctx) => {
|
|
363
|
+
// Recurrence on the `plugins` queue — deterministic idempotent jobId
|
|
364
|
+
return queueProducer.scheduleRepeatable('plugin.job', pluginJobId(ctx.tenantId, ctx.pluginName, jobName), { plugin: ctx.pluginName, job: jobName, scheduled: true }, spec, { tenantId: ctx.tenantId ?? 'default' });
|
|
365
|
+
},
|
|
366
|
+
cancel: async (jobName, ctx) => {
|
|
367
|
+
return queueProducer.cancelRepeatable('plugin.job', pluginJobId(ctx.tenantId, ctx.pluginName, jobName));
|
|
368
|
+
},
|
|
369
|
+
enqueue: async (jobName, payload, ctx) => {
|
|
370
|
+
return queueProducer.enqueue('plugin.job', { plugin: ctx.pluginName, job: jobName, data: payload }, { tenantId: ctx.tenantId ?? 'default' });
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
roles: {
|
|
374
|
+
add: (input) => pluginRolesService.add(input),
|
|
375
|
+
remove: (input) => pluginRolesService.remove(input),
|
|
376
|
+
addPermission: (input) => pluginRolesService.addPermission(input),
|
|
377
|
+
},
|
|
378
|
+
mail: {
|
|
379
|
+
send: (input) => mailService.enqueue(input),
|
|
380
|
+
},
|
|
381
|
+
audit: {
|
|
382
|
+
record: (entry) => auditService.record({
|
|
383
|
+
tenantId: entry.tenantId ?? undefined,
|
|
384
|
+
action: entry.action,
|
|
385
|
+
resourceType: entry.resourceType,
|
|
386
|
+
resourceId: entry.resourceId,
|
|
387
|
+
changes: entry.changes,
|
|
388
|
+
}),
|
|
389
|
+
},
|
|
125
390
|
};
|
|
126
391
|
}
|
|
127
392
|
function runtimeOptions(tenantId) {
|
|
@@ -143,7 +408,7 @@ function runtimeOptions(tenantId) {
|
|
|
143
408
|
* Persiste o resultado da carga em `plugins.error`/`status`:
|
|
144
409
|
* - falha nova → `status = LOAD_FAILED` + mensagem;
|
|
145
410
|
* - sucesso depois de falha → `ACTIVATED` + `error = NULL`.
|
|
146
|
-
*
|
|
411
|
+
* Only writes when the state changed (no UPDATE per request). Never throws.
|
|
147
412
|
*/
|
|
148
413
|
async function persistScopeResult(state) {
|
|
149
414
|
const sql = getConnection();
|
|
@@ -165,7 +430,7 @@ async function persistScopeResult(state) {
|
|
|
165
430
|
}
|
|
166
431
|
}
|
|
167
432
|
catch {
|
|
168
|
-
//
|
|
433
|
+
// an unavailable database cannot break the load — visibility stays in the in-memory state
|
|
169
434
|
}
|
|
170
435
|
}
|
|
171
436
|
async function syncScope(tenantId, force) {
|
|
@@ -184,12 +449,12 @@ async function syncScope(tenantId, force) {
|
|
|
184
449
|
return running;
|
|
185
450
|
const promise = (async () => {
|
|
186
451
|
try {
|
|
187
|
-
const result = await
|
|
452
|
+
const result = await (await getBootstrapPlugins())(runtimeOptions(tenantId));
|
|
188
453
|
// Coleta rotas registradas pelos plugins neste escopo
|
|
189
454
|
const scopeKey = key; // 'platform' ou 'tenant:<id>'
|
|
190
|
-
const pluginRoutes = getLoadedPluginRoutes(scopeKey);
|
|
191
|
-
for (const { pluginName, routes } of pluginRoutes) {
|
|
192
|
-
registerPluginRoutes(pluginName, routes);
|
|
455
|
+
const pluginRoutes = await (await getLoadedPluginRoutes())(scopeKey);
|
|
456
|
+
for (const { pluginName, routes, rpm } of pluginRoutes) {
|
|
457
|
+
registerPluginRoutes(pluginName, routes, rpm);
|
|
193
458
|
}
|
|
194
459
|
// Remove rotas de plugins que foram descarregados (unloaded)
|
|
195
460
|
for (const pluginName of result.unloaded) {
|
|
@@ -206,11 +471,11 @@ async function syncScope(tenantId, force) {
|
|
|
206
471
|
states.set(key, state);
|
|
207
472
|
await persistScopeResult(state);
|
|
208
473
|
state.errors = new Map(result.failed.map((failure) => [failure.name, failure.error]));
|
|
209
|
-
evictOldTenants(config.plugins.maxLoadedTenants);
|
|
474
|
+
await evictOldTenants(config.plugins.maxLoadedTenants);
|
|
210
475
|
return result;
|
|
211
476
|
}
|
|
212
477
|
catch (error) {
|
|
213
|
-
console.warn(`[plugins] sync
|
|
478
|
+
console.warn(`[plugins] sync unavailable (${key}): ${error instanceof Error ? error.message : String(error)}`);
|
|
214
479
|
const fallback = { enabled: [], loaded: [], unloaded: [], failed: [] };
|
|
215
480
|
if (!states.has(key)) {
|
|
216
481
|
states.set(key, { key, tenantId, loadedAt: 0, result: fallback, errors: new Map() });
|
|
@@ -225,7 +490,7 @@ async function syncScope(tenantId, force) {
|
|
|
225
490
|
return promise;
|
|
226
491
|
}
|
|
227
492
|
/** Evita crescer sem limite: descarta tenants com carga mais antiga. */
|
|
228
|
-
function evictOldTenants(maxTenants) {
|
|
493
|
+
async function evictOldTenants(maxTenants) {
|
|
229
494
|
if (states.size <= maxTenants)
|
|
230
495
|
return;
|
|
231
496
|
for (const [key, state] of [...states.entries()]) {
|
|
@@ -234,28 +499,28 @@ function evictOldTenants(maxTenants) {
|
|
|
234
499
|
if (state.tenantId === null)
|
|
235
500
|
continue; // platform nunca sai
|
|
236
501
|
states.delete(key);
|
|
237
|
-
unloadTenant(state.tenantId);
|
|
238
|
-
getHookRegistryStore().invalidateTenant(state.tenantId);
|
|
502
|
+
await (await unloadTenant())(state.tenantId);
|
|
503
|
+
(await (await getHookRegistryStore())).invalidateTenant(state.tenantId);
|
|
239
504
|
}
|
|
240
505
|
}
|
|
241
506
|
/**
|
|
242
507
|
* Sincroniza o registry **platform** com os plugins habilitados no banco.
|
|
243
|
-
*
|
|
244
|
-
*
|
|
508
|
+
* Implicit force: this is the explicit boot/activation call — reflects the
|
|
509
|
+
* database immediately. Idempotent and never throws.
|
|
245
510
|
*/
|
|
246
511
|
export async function syncEnabledPlugins(options = {}) {
|
|
247
512
|
try {
|
|
248
513
|
return await syncScope(null, options.force ?? true);
|
|
249
514
|
}
|
|
250
515
|
catch (error) {
|
|
251
|
-
console.warn(`[plugins] sync
|
|
516
|
+
console.warn(`[plugins] sync unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
252
517
|
return { enabled: [], loaded: [], unloaded: [], failed: [] };
|
|
253
518
|
}
|
|
254
519
|
}
|
|
255
520
|
/**
|
|
256
|
-
*
|
|
521
|
+
* Ensures the tenant's plugins are loaded into its own registry.
|
|
257
522
|
* Chamado sob demanda (middleware/API, web, worker) — com cache de TTL para
|
|
258
|
-
*
|
|
523
|
+
* avoids hitting the database on every request. Never throws.
|
|
259
524
|
*/
|
|
260
525
|
export async function ensureTenantPluginsLoaded(tenantId, options = {}) {
|
|
261
526
|
if (!tenantId)
|
|
@@ -264,29 +529,29 @@ export async function ensureTenantPluginsLoaded(tenantId, options = {}) {
|
|
|
264
529
|
return await syncScope(tenantId, options.force ?? false);
|
|
265
530
|
}
|
|
266
531
|
catch (error) {
|
|
267
|
-
console.warn(`[plugins] sync
|
|
532
|
+
console.warn(`[plugins] tenant sync unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
268
533
|
return { enabled: [], loaded: [], unloaded: [], failed: [] };
|
|
269
534
|
}
|
|
270
535
|
}
|
|
271
536
|
/**
|
|
272
|
-
*
|
|
273
|
-
*
|
|
537
|
+
* Invalidates a tenant's cache/registry (after enabling/disabling a link,
|
|
538
|
+
* uninstalling or changing permissions). The tenant's next request reloads.
|
|
274
539
|
*/
|
|
275
|
-
export function invalidateTenantPlugins(tenantId) {
|
|
540
|
+
export async function invalidateTenantPlugins(tenantId) {
|
|
276
541
|
if (!tenantId)
|
|
277
542
|
return;
|
|
278
543
|
const key = scopeKey(tenantId);
|
|
279
544
|
states.delete(key);
|
|
280
545
|
inflight.delete(key);
|
|
281
|
-
unloadTenant(tenantId);
|
|
282
|
-
getHookRegistryStore().invalidateTenant(tenantId);
|
|
546
|
+
await (await unloadTenant())(tenantId);
|
|
547
|
+
(await (await getHookRegistryStore())).invalidateTenant(tenantId);
|
|
283
548
|
}
|
|
284
|
-
/**
|
|
549
|
+
/** Invalidates the platform scope (after global changes). */
|
|
285
550
|
export function invalidatePlatformPlugins() {
|
|
286
551
|
states.delete(PLATFORM_KEY);
|
|
287
552
|
inflight.delete(PLATFORM_KEY);
|
|
288
553
|
}
|
|
289
|
-
/**
|
|
554
|
+
/** Per-scope load state — observability surface (WP2). */
|
|
290
555
|
export function getPluginsRuntimeState() {
|
|
291
556
|
return {
|
|
292
557
|
generatedAt: new Date().toISOString(),
|