@coffer-org/server 1.1.0 → 1.2.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/collection-io.d.ts +17 -13
- package/dist/collection-io.js +99 -43
- package/dist/db.d.ts +1 -1
- package/dist/db.js +22 -25
- package/dist/dialect.d.ts +4 -0
- package/dist/dialect.js +14 -0
- package/dist/embed.d.ts +14 -0
- package/dist/embed.js +46 -0
- package/dist/embeddings.d.ts +38 -0
- package/dist/embeddings.js +109 -0
- package/dist/entity-schema.d.ts +6 -17
- package/dist/entity-schema.js +103 -68
- package/dist/extend-io.d.ts +3 -4
- package/dist/extend-io.js +24 -11
- package/dist/extend-table.d.ts +5 -4
- package/dist/extend-table.js +26 -17
- package/dist/fs-list.d.ts +11 -0
- package/dist/fs-list.js +18 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +268 -190
- package/dist/local-api.d.ts +4 -5
- package/dist/local-api.js +14 -42
- package/dist/migrations.d.ts +24 -0
- package/dist/migrations.js +198 -0
- package/dist/msg-log.d.ts +12 -0
- package/dist/msg-log.js +5 -0
- package/dist/mutate.d.ts +6 -4
- package/dist/mutate.js +47 -37
- package/dist/plugin-discovery.d.ts +1 -12
- package/dist/plugin-discovery.js +15 -28
- package/dist/plugin-hooks.d.ts +36 -26
- package/dist/plugin-hooks.js +0 -2
- package/dist/plugin-runtime.d.ts +0 -23
- package/dist/plugin-runtime.js +33 -67
- package/dist/plugin-state.d.ts +2 -0
- package/dist/plugin-state.js +11 -0
- package/dist/plugins-api.d.ts +0 -1
- package/dist/plugins-api.js +2 -10
- package/dist/records-api.d.ts +19 -0
- package/dist/records-api.js +116 -0
- package/dist/registry-context.d.ts +1 -7
- package/dist/registry-context.js +4 -3
- package/dist/schema-api.d.ts +1 -0
- package/dist/schema-api.js +11 -0
- package/dist/schema-sync.d.ts +0 -1
- package/dist/schema-sync.js +1 -10
- package/dist/secrets.d.ts +7 -0
- package/dist/secrets.js +60 -0
- package/dist/seeds.d.ts +14 -0
- package/dist/seeds.js +23 -0
- package/dist/temporal.d.ts +0 -16
- package/dist/temporal.js +0 -6
- package/dist/uploads.d.ts +4 -0
- package/dist/uploads.js +12 -0
- package/package.json +12 -4
- package/dist/collection-io.d.ts.map +0 -1
- package/dist/collection-io.js.map +0 -1
- package/dist/db.d.ts.map +0 -1
- package/dist/db.js.map +0 -1
- package/dist/entity-schema.d.ts.map +0 -1
- package/dist/entity-schema.js.map +0 -1
- package/dist/extend-io.d.ts.map +0 -1
- package/dist/extend-io.js.map +0 -1
- package/dist/extend-table.d.ts.map +0 -1
- package/dist/extend-table.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/local-api.d.ts.map +0 -1
- package/dist/local-api.js.map +0 -1
- package/dist/mutate.d.ts.map +0 -1
- package/dist/mutate.js.map +0 -1
- package/dist/plugin-discovery.d.ts.map +0 -1
- package/dist/plugin-discovery.js.map +0 -1
- package/dist/plugin-hooks.d.ts.map +0 -1
- package/dist/plugin-hooks.js.map +0 -1
- package/dist/plugin-runtime.d.ts.map +0 -1
- package/dist/plugin-runtime.js.map +0 -1
- package/dist/plugins-api.d.ts.map +0 -1
- package/dist/plugins-api.js.map +0 -1
- package/dist/registry-context.d.ts.map +0 -1
- package/dist/registry-context.js.map +0 -1
- package/dist/schema-sync.d.ts.map +0 -1
- package/dist/schema-sync.js.map +0 -1
- package/dist/temporal.d.ts.map +0 -1
- package/dist/temporal.js.map +0 -1
package/dist/plugin-hooks.d.ts
CHANGED
|
@@ -1,35 +1,45 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Серверні хуки життєвого циклу плагінів (server-only).
|
|
3
|
-
*
|
|
4
|
-
* Маніфест плагіна (@coffer-org/sdk/plugin) — ізоморфний і НЕ містить функцій (щоб не тягнути
|
|
5
|
-
* серверні залежності у чисте ядро). Усе, що стосується сервера — міграції/seed/init/teardown —
|
|
6
|
-
* реєструється тут, по id плагіна.
|
|
7
|
-
*
|
|
8
|
-
* pluginHooks['people'] = {
|
|
9
|
-
* seed: async (ctx) => { ... ідемпотентне наповнення ... },
|
|
10
|
-
* init: async (ctx) => { ... запуск воркерів/перевірки ... },
|
|
11
|
-
* teardown: async (ctx) => { ... зняття (м'яке: лише cleanup стану) ... },
|
|
12
|
-
* };
|
|
13
|
-
*
|
|
14
|
-
* teardown за рішенням — М'ЯКЕ: таблиці/дані лишаються в БД,
|
|
15
|
-
* плагін лише вимикається. ctx.em — fork-нутий EntityManager для seed/init операцій.
|
|
16
|
-
*
|
|
17
|
-
* Реальні хуки оголошуються у самих плагінах (serverHooks) і збираються
|
|
18
|
-
* генерично через loadServerHooks() — тут лише контракт і порожній реєстр.
|
|
19
|
-
*/
|
|
20
1
|
import type { EntityManager } from '@mikro-orm/core';
|
|
2
|
+
import type { z } from 'zod';
|
|
3
|
+
import type { ColumnType } from '@coffer-org/sdk/fields';
|
|
4
|
+
export interface TableOps {
|
|
5
|
+
renameColumn(from: string, to: string): Promise<void>;
|
|
6
|
+
fill(column: string, value: string | number | boolean | null, opts?: {
|
|
7
|
+
where?: Record<string, string | number | boolean | null>;
|
|
8
|
+
}): Promise<void>;
|
|
9
|
+
changeType(column: string, newType: ColumnType, opts?: {
|
|
10
|
+
default?: string | number | boolean | null;
|
|
11
|
+
transform?: (old: unknown) => unknown;
|
|
12
|
+
}): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export interface MigrationCtx {
|
|
15
|
+
table(name: string): TableOps;
|
|
16
|
+
}
|
|
17
|
+
export interface Migration {
|
|
18
|
+
version: number;
|
|
19
|
+
up(ctx: MigrationCtx): Promise<void> | void;
|
|
20
|
+
}
|
|
21
21
|
export interface PluginCtx {
|
|
22
|
-
/** Fork-нутий EntityManager для seed/init операцій. */
|
|
23
22
|
em: EntityManager;
|
|
24
23
|
}
|
|
24
|
+
export interface Seed {
|
|
25
|
+
name: string;
|
|
26
|
+
run(ctx: PluginCtx): Promise<void> | void;
|
|
27
|
+
}
|
|
28
|
+
export interface AgentTool {
|
|
29
|
+
name: string;
|
|
30
|
+
description: string;
|
|
31
|
+
inputSchema: z.ZodRawShape;
|
|
32
|
+
handler: (args: Record<string, unknown>, ctx: PluginCtx) => Promise<unknown> | unknown;
|
|
33
|
+
}
|
|
34
|
+
export interface AgentContribution {
|
|
35
|
+
instructions?: string | ((ctx: PluginCtx) => string | Promise<string>);
|
|
36
|
+
tools?: AgentTool[];
|
|
37
|
+
}
|
|
25
38
|
export interface PluginHooks {
|
|
26
|
-
|
|
27
|
-
seed
|
|
28
|
-
/** Довільна ініціалізація після seed (воркери, перевірки). */
|
|
39
|
+
migrations?: Migration[];
|
|
40
|
+
seed?: Seed[];
|
|
29
41
|
init?(ctx: PluginCtx): Promise<void> | void;
|
|
30
|
-
/** Зняття плагіна (м'яке): cleanup рантайм-стану. Дані НЕ дропаються. */
|
|
31
42
|
teardown?(ctx: PluginCtx): Promise<void> | void;
|
|
43
|
+
agent?: AgentContribution;
|
|
32
44
|
}
|
|
33
|
-
/** Реєстр серверних хуків по id плагіна. Порожньо = плагін без серверної логіки. */
|
|
34
45
|
export declare const pluginHooks: Record<string, PluginHooks>;
|
|
35
|
-
//# sourceMappingURL=plugin-hooks.d.ts.map
|
package/dist/plugin-hooks.js
CHANGED
package/dist/plugin-runtime.d.ts
CHANGED
|
@@ -1,36 +1,13 @@
|
|
|
1
1
|
import { type Registry } from '@coffer-org/core/compose';
|
|
2
2
|
import type { PluginManifest } from '@coffer-org/sdk/plugin';
|
|
3
3
|
export declare function getPlugins(): Promise<PluginManifest[]>;
|
|
4
|
-
/** Прочитати множину вимкнених плагінів із _plugins (core ніколи не вимкнено). */
|
|
5
4
|
export declare function readDisabled(): Promise<Set<string>>;
|
|
6
|
-
/**
|
|
7
|
-
* Прочитати налаштування плагіна (singleton-запис у _settings__<id>).
|
|
8
|
-
* Повертає {} якщо запис ще не збережено.
|
|
9
|
-
*/
|
|
10
5
|
export declare function getPluginSettings(pluginId: string): Promise<Record<string, unknown>>;
|
|
11
|
-
/**
|
|
12
|
-
* Двофазний bootstrap БД. Повертає множину вимкнених плагінів.
|
|
13
|
-
* Викликається сервером (через initPlugins) і CLI-скриптами напряму.
|
|
14
|
-
*/
|
|
15
6
|
export declare function initStorage(): Promise<Set<string>>;
|
|
16
7
|
export declare function initPlugins(): Promise<Registry>;
|
|
17
|
-
/**
|
|
18
|
-
* Graceful-зупинка воркерів активних плагінів (виклик teardown) на shutdown сервера.
|
|
19
|
-
* Зворотний порядок відносно init. Помилки окремого плагіна не зупиняють решту.
|
|
20
|
-
*/
|
|
21
8
|
export declare function teardownPlugins(): Promise<void>;
|
|
22
|
-
/** М'яке зняття плагіна: лише cleanup-хук. Таблиці/дані НЕ дропаються. */
|
|
23
9
|
export declare function teardownPlugin(id: string): Promise<void>;
|
|
24
|
-
/**
|
|
25
|
-
* ЯВНЕ видалення даних плагіна (безповоротне):
|
|
26
|
-
* teardown-хук → DROP TABLE (модулі + extends) → enabled=0 → аудит.
|
|
27
|
-
* Ідемпотентно: повторний виклик безпечний (IF EXISTS, findOne).
|
|
28
|
-
* DDL у SQLite через knex виконується в авто-комміті, тому атомарності
|
|
29
|
-
* між дропами нема — натомість послідовність безпечно перезапускається.
|
|
30
|
-
*/
|
|
31
10
|
export declare function purgePluginData(p: PluginManifest, actor: string): Promise<void>;
|
|
32
|
-
/** Pure filter: drop disabled ids, preserve input order. */
|
|
33
11
|
export declare function filterEnabled<T extends {
|
|
34
12
|
id: string;
|
|
35
13
|
}>(all: T[], disabled: Set<string>): T[];
|
|
36
|
-
//# sourceMappingURL=plugin-runtime.d.ts.map
|
package/dist/plugin-runtime.js
CHANGED
|
@@ -1,33 +1,18 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Оркестрація життєвого циклу плагінів на старті сервера (Рівень 1: набір — з БД).
|
|
3
|
-
*
|
|
4
|
-
* initStorage() — двофазний bootstrap БД:
|
|
5
|
-
* 1. initDb(системні схеми) → syncSchema() — _events/_folders/_plugins існують;
|
|
6
|
-
* 2. seed рядків _plugins (нові → enabled=1) → читання вимкнених;
|
|
7
|
-
* 3. discoverEntity(схеми АКТИВНИХ плагінів) → syncSchema() — таблиці плагінів.
|
|
8
|
-
* Entities вимкнених плагінів НЕ реєструються: їхні таблиці невідомі ORM,
|
|
9
|
-
* тому dropTables:false гарантує збереження даних (м'яке вимкнення).
|
|
10
|
-
*
|
|
11
|
-
* initPlugins() = initStorage() + composeRegistry + seed/init хуки.
|
|
12
|
-
* purgePluginData() — ЯВНЕ видалення даних плагіна (DROP TABLE + _folders + enabled=0).
|
|
13
|
-
*
|
|
14
|
-
* core вимкнути не можна.
|
|
15
|
-
*/
|
|
16
|
-
import { randomUUID } from 'node:crypto';
|
|
17
1
|
import { serialize } from '@mikro-orm/core';
|
|
18
2
|
import { composeRegistry } from '@coffer-org/core/compose';
|
|
19
|
-
import { initDb, getOrm, getEm } from
|
|
20
|
-
import { syncSchema } from
|
|
21
|
-
import { systemEntities, buildPluginEntities, moduleTableName } from
|
|
22
|
-
import { pluginHooks } from
|
|
23
|
-
import { discoverPlugins, loadServerHooks } from
|
|
24
|
-
import {
|
|
25
|
-
|
|
3
|
+
import { initDb, getOrm, getEm } from "./db.js";
|
|
4
|
+
import { syncSchema } from "./schema-sync.js";
|
|
5
|
+
import { systemEntities, buildPluginEntities, moduleTableName } from "./entity-schema.js";
|
|
6
|
+
import { pluginHooks } from "./plugin-hooks.js";
|
|
7
|
+
import { discoverPlugins, loadServerHooks } from "./plugin-discovery.js";
|
|
8
|
+
import { runMigrations, assertSafeRequired } from "./migrations.js";
|
|
9
|
+
import { runSeeds } from "./seeds.js";
|
|
10
|
+
import { setActiveRegistry } from "./registry-context.js";
|
|
11
|
+
import { migrateEmbeddingVectorsToBlob } from "./embeddings.js";
|
|
26
12
|
let _plugins = null;
|
|
27
13
|
export async function getPlugins() {
|
|
28
14
|
return (_plugins ??= await discoverPlugins());
|
|
29
15
|
}
|
|
30
|
-
/** Прочитати множину вимкнених плагінів із _plugins (core ніколи не вимкнено). */
|
|
31
16
|
export async function readDisabled() {
|
|
32
17
|
const fork = getEm().fork();
|
|
33
18
|
const rows = await fork.find('_Plugin', { enabled: 0 });
|
|
@@ -35,10 +20,6 @@ export async function readDisabled() {
|
|
|
35
20
|
disabled.delete('core');
|
|
36
21
|
return disabled;
|
|
37
22
|
}
|
|
38
|
-
/**
|
|
39
|
-
* Прочитати налаштування плагіна (singleton-запис у _settings__<id>).
|
|
40
|
-
* Повертає {} якщо запис ще не збережено.
|
|
41
|
-
*/
|
|
42
23
|
export async function getPluginSettings(pluginId) {
|
|
43
24
|
try {
|
|
44
25
|
const fork = getEm().fork();
|
|
@@ -53,7 +34,6 @@ export async function getPluginSettings(pluginId) {
|
|
|
53
34
|
return {};
|
|
54
35
|
}
|
|
55
36
|
}
|
|
56
|
-
/** Зареєструвати рядки _plugins для всіх відомих плагінів (нові → enabled=1). */
|
|
57
37
|
async function seedPluginRows() {
|
|
58
38
|
const fork = getEm().fork();
|
|
59
39
|
const existing = new Set((await fork.find('_Plugin', {})).map((r) => r.id));
|
|
@@ -65,24 +45,25 @@ async function seedPluginRows() {
|
|
|
65
45
|
}
|
|
66
46
|
await fork.flush();
|
|
67
47
|
}
|
|
68
|
-
/**
|
|
69
|
-
* Двофазний bootstrap БД. Повертає множину вимкнених плагінів.
|
|
70
|
-
* Викликається сервером (через initPlugins) і CLI-скриптами напряму.
|
|
71
|
-
*/
|
|
72
48
|
export async function initStorage() {
|
|
73
49
|
await initDb(systemEntities);
|
|
74
|
-
await syncSchema();
|
|
75
|
-
|
|
76
|
-
// модулів дропає syncSchema (safe:false); таблицю _folders — лише явний DROP
|
|
77
|
-
// (dropTables:false таблиці не чіпає). Ідемпотентно (IF EXISTS).
|
|
50
|
+
await syncSchema();
|
|
51
|
+
await migrateEmbeddingVectorsToBlob();
|
|
78
52
|
await getEm().fork().getConnection().execute('DROP TABLE IF EXISTS "_folders"');
|
|
79
53
|
await seedPluginRows();
|
|
80
54
|
const disabled = await readDisabled();
|
|
81
55
|
const active = (await getPlugins()).filter((p) => !disabled.has(p.id));
|
|
56
|
+
const hooks = await loadServerHooks();
|
|
57
|
+
await runMigrations({
|
|
58
|
+
em: getEm().fork(),
|
|
59
|
+
plugins: active.map((p) => ({ id: p.id, tables: pluginTables(p) })),
|
|
60
|
+
hooks,
|
|
61
|
+
});
|
|
82
62
|
const pluginEntities = buildPluginEntities(active);
|
|
63
|
+
await assertSafeRequired(getEm().fork(), pluginEntities);
|
|
83
64
|
if (pluginEntities.length)
|
|
84
65
|
getOrm().discoverEntity(pluginEntities);
|
|
85
|
-
await syncSchema();
|
|
66
|
+
await syncSchema();
|
|
86
67
|
return disabled;
|
|
87
68
|
}
|
|
88
69
|
export async function initPlugins() {
|
|
@@ -90,31 +71,24 @@ export async function initPlugins() {
|
|
|
90
71
|
const reg = composeRegistry(await getPlugins(), { disabled });
|
|
91
72
|
setActiveRegistry(reg);
|
|
92
73
|
Object.assign(pluginHooks, await loadServerHooks());
|
|
74
|
+
await runSeeds({ em: getEm().fork(), plugins: reg.order, hooks: pluginHooks });
|
|
93
75
|
for (const p of reg.order) {
|
|
94
76
|
const h = pluginHooks[p.id];
|
|
95
77
|
try {
|
|
96
|
-
if (h?.seed) {
|
|
97
|
-
await h.seed({ em: getEm().fork() });
|
|
98
|
-
console.log(`[plugin] ${p.id}: seed ✓`);
|
|
99
|
-
}
|
|
100
78
|
if (h?.init) {
|
|
101
79
|
await h.init({ em: getEm().fork() });
|
|
102
80
|
console.log(`[plugin] ${p.id}: init ✓`);
|
|
103
81
|
}
|
|
104
82
|
}
|
|
105
83
|
catch (err) {
|
|
106
|
-
console.error(`[plugin] ${p.id}:
|
|
84
|
+
console.error(`[plugin] ${p.id}: initialization failure`, err);
|
|
107
85
|
throw err;
|
|
108
86
|
}
|
|
109
87
|
}
|
|
110
|
-
console.log(`[plugin]
|
|
111
|
-
(disabled.size ? ` |
|
|
88
|
+
console.log(`[plugin] active: ${reg.order.map((p) => p.id).join(' ')}` +
|
|
89
|
+
(disabled.size ? ` | disabled: ${[...disabled].join(' ')}` : ''));
|
|
112
90
|
return reg;
|
|
113
91
|
}
|
|
114
|
-
/**
|
|
115
|
-
* Graceful-зупинка воркерів активних плагінів (виклик teardown) на shutdown сервера.
|
|
116
|
-
* Зворотний порядок відносно init. Помилки окремого плагіна не зупиняють решту.
|
|
117
|
-
*/
|
|
118
92
|
export async function teardownPlugins() {
|
|
119
93
|
Object.assign(pluginHooks, await loadServerHooks());
|
|
120
94
|
const disabled = await readDisabled();
|
|
@@ -132,7 +106,6 @@ export async function teardownPlugins() {
|
|
|
132
106
|
}
|
|
133
107
|
}
|
|
134
108
|
}
|
|
135
|
-
/** М'яке зняття плагіна: лише cleanup-хук. Таблиці/дані НЕ дропаються. */
|
|
136
109
|
export async function teardownPlugin(id) {
|
|
137
110
|
const h = pluginHooks[id];
|
|
138
111
|
if (h?.teardown) {
|
|
@@ -140,56 +113,49 @@ export async function teardownPlugin(id) {
|
|
|
140
113
|
console.log(`[plugin] ${id}: teardown ✓`);
|
|
141
114
|
}
|
|
142
115
|
}
|
|
143
|
-
/** Пари (vault, module) усіх модулів плагіна — власні vault'и + vaultModules. */
|
|
144
116
|
function pluginModules(p) {
|
|
145
117
|
return [
|
|
146
118
|
...(p.vaults ?? []).flatMap((v) => v.modules.map((m) => ({ vault: m.vault, module: m.module }))),
|
|
147
119
|
...(p.vaultModules ?? []).map((m) => ({ vault: m.vault, module: m.module })),
|
|
148
120
|
];
|
|
149
121
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
* teardown-хук → DROP TABLE (модулі + extends) → enabled=0 → аудит.
|
|
153
|
-
* Ідемпотентно: повторний виклик безпечний (IF EXISTS, findOne).
|
|
154
|
-
* DDL у SQLite через knex виконується в авто-комміті, тому атомарності
|
|
155
|
-
* між дропами нема — натомість послідовність безпечно перезапускається.
|
|
156
|
-
*/
|
|
157
|
-
export async function purgePluginData(p, actor) {
|
|
158
|
-
await teardownPlugin(p.id);
|
|
159
|
-
const tables = [
|
|
122
|
+
function pluginTables(p) {
|
|
123
|
+
return [
|
|
160
124
|
...pluginModules(p).map(({ vault, module }) => moduleTableName(vault, module)),
|
|
161
125
|
...(p.extends_ ?? []).map((e) => `extend__${e.id}`),
|
|
162
126
|
...(p.settings && p.settings.fields.length > 0 ? [`_settings__${p.id}`] : []),
|
|
163
127
|
];
|
|
128
|
+
}
|
|
129
|
+
export async function purgePluginData(p, actor) {
|
|
130
|
+
await teardownPlugin(p.id);
|
|
131
|
+
const tables = pluginTables(p);
|
|
164
132
|
const conn = getEm().fork().getConnection();
|
|
165
133
|
for (const t of tables) {
|
|
166
134
|
await conn.execute(`DROP TABLE IF EXISTS "${t}"`);
|
|
167
135
|
}
|
|
168
|
-
// enabled=0 + аудит — однією транзакцією.
|
|
169
136
|
await getEm()
|
|
170
137
|
.fork()
|
|
171
138
|
.transactional(async (tx) => {
|
|
172
139
|
const now = new Date().toISOString();
|
|
140
|
+
await tx.nativeDelete('_Migration', { plugin_id: p.id });
|
|
141
|
+
await tx.nativeDelete('_Seed', { plugin_id: p.id });
|
|
173
142
|
const row = await tx.findOne('_Plugin', { id: p.id });
|
|
174
143
|
if (row)
|
|
175
144
|
tx.assign(row, { enabled: 0, updated_at: now });
|
|
176
145
|
else
|
|
177
146
|
tx.create('_Plugin', { id: p.id, enabled: 0, updated_at: now });
|
|
178
147
|
tx.create('_Event', {
|
|
179
|
-
id: randomUUID(),
|
|
180
148
|
ts: now,
|
|
181
149
|
actor,
|
|
182
150
|
op: 'purge',
|
|
183
151
|
type: p.id,
|
|
184
|
-
record_id:
|
|
152
|
+
record_id: null,
|
|
185
153
|
before: null,
|
|
186
154
|
after: null,
|
|
187
155
|
});
|
|
188
156
|
});
|
|
189
|
-
console.log(`[plugin] ${p.id}: purge ✓ (${tables.length}
|
|
157
|
+
console.log(`[plugin] ${p.id}: purge ✓ (${tables.length} tables)`);
|
|
190
158
|
}
|
|
191
|
-
/** Pure filter: drop disabled ids, preserve input order. */
|
|
192
159
|
export function filterEnabled(all, disabled) {
|
|
193
160
|
return all.filter((p) => !disabled.has(p.id));
|
|
194
161
|
}
|
|
195
|
-
//# sourceMappingURL=plugin-runtime.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getEm } from "./db.js";
|
|
2
|
+
export async function getPluginState(plugin, key) {
|
|
3
|
+
const em = getEm().fork();
|
|
4
|
+
const row = (await em.findOne('_PluginState', { plugin, key }));
|
|
5
|
+
return row?.value ?? null;
|
|
6
|
+
}
|
|
7
|
+
export async function setPluginState(plugin, key, value) {
|
|
8
|
+
const em = getEm().fork();
|
|
9
|
+
await em.upsert('_PluginState', { plugin, key, value });
|
|
10
|
+
await em.flush();
|
|
11
|
+
}
|
package/dist/plugins-api.d.ts
CHANGED
package/dist/plugins-api.js
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { createReadStream, existsSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { discoverPluginAssets } from
|
|
4
|
-
import { getPlugins, readDisabled } from
|
|
3
|
+
import { discoverPluginAssets } from "./plugin-discovery.js";
|
|
4
|
+
import { getPlugins, readDisabled } from "./plugin-runtime.js";
|
|
5
5
|
const nmRoot = () => join(process.cwd(), 'node_modules');
|
|
6
|
-
// requested filename → key in package.json#coffer
|
|
7
6
|
const ASSET_KEY = { 'schema.js': 'schema', 'web.js': 'web', 'web.css': 'css' };
|
|
8
7
|
export async function registerPluginsApi(app) {
|
|
9
|
-
// Single /api/plugins manifest serving TWO consumers:
|
|
10
|
-
// • the web federation loader — reads `enabled` + `schema`/`web`/`css` URLs;
|
|
11
|
-
// • the admin SettingsPage — reads `enabled`/`vaults`/`extends`/`hasSettings`.
|
|
12
|
-
// Returns ALL discovered plugins (loader filters by `enabled` itself). Toggling
|
|
13
|
-
// is the canonical PATCH /api/plugins/:id (dependency-validated, in index.ts).
|
|
14
8
|
app.get('/api/plugins', async () => {
|
|
15
9
|
const [plugins, assets, disabled] = await Promise.all([getPlugins(), discoverPluginAssets(), readDisabled()]);
|
|
16
10
|
const assetById = new Map(assets.map((a) => [a.id, a]));
|
|
@@ -30,7 +24,6 @@ export async function registerPluginsApi(app) {
|
|
|
30
24
|
};
|
|
31
25
|
});
|
|
32
26
|
});
|
|
33
|
-
// Serve a plugin's built asset from node_modules: /plugins/<id>/<file>
|
|
34
27
|
app.get('/plugins/:id/:file', async (req, reply) => {
|
|
35
28
|
const { id, file } = req.params;
|
|
36
29
|
const key = ASSET_KEY[file];
|
|
@@ -50,4 +43,3 @@ export async function registerPluginsApi(app) {
|
|
|
50
43
|
return reply.send(createReadStream(abs));
|
|
51
44
|
});
|
|
52
45
|
}
|
|
53
|
-
//# sourceMappingURL=plugins-api.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ModuleDef } from '@coffer-org/sdk/module';
|
|
2
|
+
export declare class UnknownTypeError extends Error {
|
|
3
|
+
}
|
|
4
|
+
export type RecordListQuery = {
|
|
5
|
+
q?: string;
|
|
6
|
+
id?: string;
|
|
7
|
+
[field: string]: string | number | undefined;
|
|
8
|
+
};
|
|
9
|
+
export declare function coerceFilter(v: string, column: string): unknown;
|
|
10
|
+
export declare function withExtends(record: Record<string, unknown>, vault: string, module: string): Promise<Record<string, unknown>>;
|
|
11
|
+
export declare function rowMatch(mdef: ModuleDef, row: Record<string, unknown>, tokens: string[]): {
|
|
12
|
+
score: number;
|
|
13
|
+
snippet: string;
|
|
14
|
+
} | null;
|
|
15
|
+
export declare function recordList(vault: string, type: string, query?: RecordListQuery): Promise<Record<string, unknown>[]>;
|
|
16
|
+
export declare function recordGet(vault: string, type: string, id: number): Promise<Record<string, unknown> | null>;
|
|
17
|
+
export declare function recordCreate(vault: string, type: string, body: unknown, actor?: string): Promise<Record<string, unknown>>;
|
|
18
|
+
export declare function recordUpdate(vault: string, type: string, id: number, body: unknown, actor?: string): Promise<Record<string, unknown>>;
|
|
19
|
+
export declare function recordDelete(vault: string, type: string, id: number, actor?: string): Promise<void>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { fieldMap, textSearchKeys, titleKey, recordTitle } from '@coffer-org/sdk/module';
|
|
2
|
+
import { tokenize, matchScoreFolded, foldText } from '@coffer-org/core/search';
|
|
3
|
+
import { serialize } from '@mikro-orm/core';
|
|
4
|
+
import { getModule, getExtendsFor } from "./registry-context.js";
|
|
5
|
+
import { moduleTableName } from "./entity-schema.js";
|
|
6
|
+
import { getExtendRecord } from "./extend-table.js";
|
|
7
|
+
import { getEm } from "./db.js";
|
|
8
|
+
import { decodeTemporal, dtStringToDate } from "./temporal.js";
|
|
9
|
+
import { splitBody, saveExtends, deleteExtends, validateExtends } from "./extend-io.js";
|
|
10
|
+
import { createRecord, updateRecord, getRecord, deleteRecord } from "./mutate.js";
|
|
11
|
+
export class UnknownTypeError extends Error {
|
|
12
|
+
}
|
|
13
|
+
function resolve(vault, type) {
|
|
14
|
+
const m = getModule(vault, type);
|
|
15
|
+
if (!m)
|
|
16
|
+
throw new UnknownTypeError(`unknown_type ${vault}/${type}`);
|
|
17
|
+
return { m, ename: moduleTableName(m.vault, m.module) };
|
|
18
|
+
}
|
|
19
|
+
export function coerceFilter(v, column) {
|
|
20
|
+
if (column === 'integer' || column === 'real') {
|
|
21
|
+
const n = Number(v);
|
|
22
|
+
return Number.isNaN(n) ? v : n;
|
|
23
|
+
}
|
|
24
|
+
if (column === 'boolean')
|
|
25
|
+
return v === 'true' || v === '1';
|
|
26
|
+
if (column === 'datetime')
|
|
27
|
+
return dtStringToDate(v);
|
|
28
|
+
return v;
|
|
29
|
+
}
|
|
30
|
+
export async function withExtends(record, vault, module) {
|
|
31
|
+
const matchedExtends = getExtendsFor(vault, module);
|
|
32
|
+
if (!matchedExtends.length)
|
|
33
|
+
return record;
|
|
34
|
+
const _extends = {};
|
|
35
|
+
await Promise.all(matchedExtends.map(async (e) => {
|
|
36
|
+
_extends[e.id] = (await getExtendRecord(e, record.id)) ?? null;
|
|
37
|
+
}));
|
|
38
|
+
return { ...record, _extends };
|
|
39
|
+
}
|
|
40
|
+
export function rowMatch(mdef, row, tokens) {
|
|
41
|
+
const textKeys = textSearchKeys(mdef);
|
|
42
|
+
if (!textKeys.length)
|
|
43
|
+
return null;
|
|
44
|
+
const tk = titleKey(mdef);
|
|
45
|
+
const otherKeys = textKeys.filter((key) => key !== tk);
|
|
46
|
+
const label = recordTitle(mdef, row);
|
|
47
|
+
const otherRaw = otherKeys.map((key) => String(row[key] ?? ''));
|
|
48
|
+
const foldedOthers = otherRaw.map(foldText);
|
|
49
|
+
const score = matchScoreFolded(foldText(label), foldedOthers, tokens);
|
|
50
|
+
if (score === null)
|
|
51
|
+
return null;
|
|
52
|
+
const hitIdx = foldedOthers.findIndex((f) => tokens.some((t) => f.includes(t)));
|
|
53
|
+
const raw = (hitIdx >= 0 ? otherRaw[hitIdx] : label) ?? label;
|
|
54
|
+
const snippet = raw.length > 120 ? raw.slice(0, 120) + '…' : raw;
|
|
55
|
+
return { score, snippet };
|
|
56
|
+
}
|
|
57
|
+
export async function recordList(vault, type, query = {}) {
|
|
58
|
+
const { m, ename } = resolve(vault, type);
|
|
59
|
+
const { q, ...filterParams } = query;
|
|
60
|
+
const where = {};
|
|
61
|
+
const fm = fieldMap(m.fields);
|
|
62
|
+
for (const [k, v] of Object.entries(filterParams)) {
|
|
63
|
+
if (k === 'id' || v === undefined)
|
|
64
|
+
continue;
|
|
65
|
+
const fieldDef = fm[k];
|
|
66
|
+
if (fieldDef && !fieldDef.virtual)
|
|
67
|
+
where[k] = coerceFilter(String(v), fieldDef.column);
|
|
68
|
+
}
|
|
69
|
+
if (filterParams.id) {
|
|
70
|
+
const ids = String(filterParams.id)
|
|
71
|
+
.split(',')
|
|
72
|
+
.map((s) => Number(s.trim()))
|
|
73
|
+
.filter((n) => !Number.isNaN(n));
|
|
74
|
+
if (ids.length)
|
|
75
|
+
where['id'] = ids.length === 1 ? ids[0] : { $in: ids };
|
|
76
|
+
}
|
|
77
|
+
if (m.standalone === false && Object.keys(where).length === 0)
|
|
78
|
+
return [];
|
|
79
|
+
const fork = getEm().fork();
|
|
80
|
+
let rows = (await fork.find(ename, where)).map((r) => serialize(r));
|
|
81
|
+
const tokens = typeof q === 'string' && q ? tokenize(q) : [];
|
|
82
|
+
if (tokens.length > 0)
|
|
83
|
+
rows = rows.filter((row) => rowMatch(m, row, tokens) !== null);
|
|
84
|
+
const decoded = rows.map((r) => decodeTemporal(m, r));
|
|
85
|
+
return Promise.all(decoded.map((row) => withExtends(row, vault, type)));
|
|
86
|
+
}
|
|
87
|
+
export async function recordGet(vault, type, id) {
|
|
88
|
+
const { m, ename } = resolve(vault, type);
|
|
89
|
+
if (m.standalone === false)
|
|
90
|
+
return null;
|
|
91
|
+
const row = await getRecord(m, ename, id);
|
|
92
|
+
if (!row)
|
|
93
|
+
return null;
|
|
94
|
+
return withExtends(row, vault, type);
|
|
95
|
+
}
|
|
96
|
+
export async function recordCreate(vault, type, body, actor = 'gui') {
|
|
97
|
+
const { m, ename } = resolve(vault, type);
|
|
98
|
+
const { base, extData } = splitBody(body);
|
|
99
|
+
validateExtends(vault, type, extData);
|
|
100
|
+
const row = await createRecord(m, ename, base, { actor });
|
|
101
|
+
await saveExtends(vault, type, row.id, extData);
|
|
102
|
+
return withExtends(row, vault, type);
|
|
103
|
+
}
|
|
104
|
+
export async function recordUpdate(vault, type, id, body, actor = 'gui') {
|
|
105
|
+
const { m, ename } = resolve(vault, type);
|
|
106
|
+
const { base, extData } = splitBody(body);
|
|
107
|
+
validateExtends(vault, type, extData);
|
|
108
|
+
const row = await updateRecord(m, ename, id, base, { actor });
|
|
109
|
+
await saveExtends(vault, type, id, extData);
|
|
110
|
+
return withExtends(row, vault, type);
|
|
111
|
+
}
|
|
112
|
+
export async function recordDelete(vault, type, id, actor = 'gui') {
|
|
113
|
+
const { m, ename } = resolve(vault, type);
|
|
114
|
+
await deleteRecord(m, ename, id, { actor });
|
|
115
|
+
await deleteExtends(vault, type, id);
|
|
116
|
+
}
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Серверний реєстр-контекст: активний composeRegistry, заданий на boot (initPlugins).
|
|
3
|
-
* Розриває цикл server→@registry(app root)→plugins→server. getModule/getExtendsFor
|
|
4
|
-
* беруться звідси (делегують у рантайм-реєстр), а не зі статичного src/registry.ts.
|
|
5
|
-
*/
|
|
6
1
|
import type { Registry } from '@coffer-org/core/compose';
|
|
7
|
-
/** Встановлюється раз на boot із initPlugins(). */
|
|
8
2
|
export declare function setActiveRegistry(reg: Registry): void;
|
|
3
|
+
export declare function getActiveRegistry(): Registry;
|
|
9
4
|
export declare const getModule: Registry['getModule'];
|
|
10
5
|
export declare const getExtendsFor: Registry['getExtendsFor'];
|
|
11
|
-
//# sourceMappingURL=registry-context.d.ts.map
|
package/dist/registry-context.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
let active = null;
|
|
2
|
-
/** Встановлюється раз на boot із initPlugins(). */
|
|
3
2
|
export function setActiveRegistry(reg) {
|
|
4
3
|
active = reg;
|
|
5
4
|
}
|
|
6
5
|
function current() {
|
|
7
6
|
if (!active)
|
|
8
|
-
throw new Error('[registry-context]
|
|
7
|
+
throw new Error('[registry-context] registry not initialized yet (call setActiveRegistry)');
|
|
9
8
|
return active;
|
|
10
9
|
}
|
|
10
|
+
export function getActiveRegistry() {
|
|
11
|
+
return current();
|
|
12
|
+
}
|
|
11
13
|
export const getModule = (vault, module) => current().getModule(vault, module);
|
|
12
14
|
export const getExtendsFor = (vault, module) => current().getExtendsFor(vault, module);
|
|
13
|
-
//# sourceMappingURL=registry-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function buildClientSchema(): unknown[];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { moduleToClient } from '@coffer-org/sdk/module';
|
|
2
|
+
import { extendToClient } from '@coffer-org/sdk/extend';
|
|
3
|
+
import { getActiveRegistry } from "./registry-context.js";
|
|
4
|
+
export function buildClientSchema() {
|
|
5
|
+
const { vaults, extends_ } = getActiveRegistry();
|
|
6
|
+
return vaults.map((v) => ({
|
|
7
|
+
...v.meta,
|
|
8
|
+
modules: v.modules.map(moduleToClient),
|
|
9
|
+
extends: extends_.filter((e) => e.attachTo.some((r) => r.vault === v.meta.id)).map(extendToClient),
|
|
10
|
+
}));
|
|
11
|
+
}
|
package/dist/schema-sync.d.ts
CHANGED
package/dist/schema-sync.js
CHANGED
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Синхронізація схеми БД зі схемами сутностей у рантаймі (без міграцій).
|
|
3
|
-
*
|
|
4
|
-
* safe:false — повний синк колонок ВІДОМИХ таблиць (додавання + видалення);
|
|
5
|
-
* видалене з f.* поле → колонка дропається при наступному старті.
|
|
6
|
-
* dropTables:false — таблиці, невідомі ORM (purge-нуті/вимкнені плагіни, legacy),
|
|
7
|
-
* не чіпаються ніколи. Дроп таблиць — лише явний purge.
|
|
8
|
-
*/
|
|
9
|
-
import { getOrm } from './db.js';
|
|
1
|
+
import { getOrm } from "./db.js";
|
|
10
2
|
export async function syncSchema() {
|
|
11
3
|
await getOrm().schema.update({ safe: false, dropTables: false });
|
|
12
4
|
}
|
|
13
|
-
//# sourceMappingURL=schema-sync.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { LayoutEl } from '@coffer-org/sdk/fields';
|
|
2
|
+
export declare const MASK = "********";
|
|
3
|
+
export declare function passwordKeys(fields: LayoutEl[]): string[];
|
|
4
|
+
export declare function maskSecrets(fields: LayoutEl[], row: Record<string, unknown>): Record<string, unknown>;
|
|
5
|
+
export declare function preserveSecrets(fields: LayoutEl[], incoming: Record<string, unknown>, existing: Record<string, unknown>): Record<string, unknown>;
|
|
6
|
+
export declare function maskTree(fields: LayoutEl[], obj: Record<string, unknown>): Record<string, unknown>;
|
|
7
|
+
export declare function preserveTree(fields: LayoutEl[], incoming: Record<string, unknown>, existing: Record<string, unknown>): Record<string, unknown>;
|
package/dist/secrets.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { fieldEntries, collectionGroups } from '@coffer-org/sdk/module';
|
|
2
|
+
export const MASK = '********';
|
|
3
|
+
export function passwordKeys(fields) {
|
|
4
|
+
return fieldEntries(fields)
|
|
5
|
+
.filter(([, f]) => f.kind === 'password')
|
|
6
|
+
.map(([k]) => k);
|
|
7
|
+
}
|
|
8
|
+
export function maskSecrets(fields, row) {
|
|
9
|
+
const out = { ...row };
|
|
10
|
+
for (const k of passwordKeys(fields))
|
|
11
|
+
if (out[k])
|
|
12
|
+
out[k] = MASK;
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
export function preserveSecrets(fields, incoming, existing) {
|
|
16
|
+
const out = { ...incoming };
|
|
17
|
+
for (const k of passwordKeys(fields)) {
|
|
18
|
+
if (out[k] !== MASK)
|
|
19
|
+
continue;
|
|
20
|
+
if (existing[k] !== undefined && existing[k] !== null)
|
|
21
|
+
out[k] = existing[k];
|
|
22
|
+
else
|
|
23
|
+
delete out[k];
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
export function maskTree(fields, obj) {
|
|
28
|
+
const out = maskSecrets(fields, obj);
|
|
29
|
+
for (const c of collectionGroups(fields)) {
|
|
30
|
+
const key = c.key;
|
|
31
|
+
const sub = c.group.fields;
|
|
32
|
+
const arr = out[key];
|
|
33
|
+
if (Array.isArray(arr)) {
|
|
34
|
+
out[key] = arr.map((item) => item && typeof item === 'object' ? maskTree(sub, item) : item);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
export function preserveTree(fields, incoming, existing) {
|
|
40
|
+
const out = preserveSecrets(fields, incoming, existing);
|
|
41
|
+
for (const c of collectionGroups(fields)) {
|
|
42
|
+
const key = c.key;
|
|
43
|
+
const sub = c.group.fields;
|
|
44
|
+
const uniq = c.group.unique ?? [];
|
|
45
|
+
const inArr = out[key];
|
|
46
|
+
if (!Array.isArray(inArr))
|
|
47
|
+
continue;
|
|
48
|
+
const exArr = Array.isArray(existing[key]) ? existing[key] : [];
|
|
49
|
+
out[key] = inArr.map((item) => {
|
|
50
|
+
if (!item || typeof item !== 'object')
|
|
51
|
+
return item;
|
|
52
|
+
const rec = item;
|
|
53
|
+
const match = uniq.length > 0
|
|
54
|
+
? exArr.find((e) => e && uniq.every((u) => e[u] === rec[u]))
|
|
55
|
+
: undefined;
|
|
56
|
+
return preserveTree(sub, rec, (match ?? {}));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|