@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/local-api.js
CHANGED
|
@@ -1,46 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* Потрібен для імпорту/сидів у середовищах, де піднятий сервер не годиться
|
|
4
|
-
* (напр. пісочниця, що скидає overlay вбитого процесу): процес-писар сам
|
|
5
|
-
* мутує БД і чисто завершується → дані персистяться.
|
|
6
|
-
*
|
|
7
|
-
* Повторює логіку app.post/app.patch з index.ts: split _extend_<id> → base,
|
|
8
|
-
* createRecord/updateRecord (mutate), upsert extends.
|
|
9
|
-
*/
|
|
10
|
-
import { getModule } from './registry-context.js';
|
|
11
|
-
import { createRecord, updateRecord, getRecord } from './mutate.js';
|
|
12
|
-
import { moduleTableName } from './entity-schema.js';
|
|
13
|
-
import { splitBody, saveExtends } from './extend-io.js';
|
|
14
|
-
/** Ім'я таблиці для (vault, type), якщо модуль відомий. Реєстр in-memory — без кешу. */
|
|
15
|
-
function enameFor(vault, type) {
|
|
16
|
-
const m = getModule(vault, type);
|
|
17
|
-
return m ? moduleTableName(m.vault, m.module) : undefined;
|
|
18
|
-
}
|
|
19
|
-
export class UnknownTypeError extends Error {
|
|
20
|
-
}
|
|
1
|
+
import { recordCreate, recordUpdate, recordGet, UnknownTypeError } from "./records-api.js";
|
|
2
|
+
export { UnknownTypeError };
|
|
21
3
|
export async function localExists(vault, type, id) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
4
|
+
try {
|
|
5
|
+
return Boolean(await recordGet(vault, type, id));
|
|
6
|
+
}
|
|
7
|
+
catch (e) {
|
|
8
|
+
if (e instanceof UnknownTypeError)
|
|
9
|
+
return false;
|
|
10
|
+
throw e;
|
|
11
|
+
}
|
|
25
12
|
}
|
|
26
|
-
export
|
|
27
|
-
|
|
28
|
-
const ename = enameFor(vault, type);
|
|
29
|
-
if (!m || !ename)
|
|
30
|
-
throw new UnknownTypeError(`unknown_type ${vault}/${type}`);
|
|
31
|
-
const { base, extData } = splitBody(body);
|
|
32
|
-
const row = await createRecord(m, ename, base, { actor: 'import' });
|
|
33
|
-
await saveExtends(vault, type, String(row.id), extData);
|
|
34
|
-
return row;
|
|
13
|
+
export function localPost(vault, type, body) {
|
|
14
|
+
return recordCreate(vault, type, body, 'import');
|
|
35
15
|
}
|
|
36
|
-
export
|
|
37
|
-
|
|
38
|
-
const ename = enameFor(vault, type);
|
|
39
|
-
if (!m || !ename)
|
|
40
|
-
throw new UnknownTypeError(`unknown_type ${vault}/${type}`);
|
|
41
|
-
const { base, extData } = splitBody(body);
|
|
42
|
-
const row = await updateRecord(m, ename, id, base, { actor: 'import' });
|
|
43
|
-
await saveExtends(vault, type, id, extData);
|
|
44
|
-
return row;
|
|
16
|
+
export function localPatch(vault, type, id, body) {
|
|
17
|
+
return recordUpdate(vault, type, id, body, 'import');
|
|
45
18
|
}
|
|
46
|
-
//# sourceMappingURL=local-api.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { EntityManager, EntitySchema } from '@mikro-orm/core';
|
|
2
|
+
import type { Migration, PluginHooks, TableOps } from './plugin-hooks.ts';
|
|
3
|
+
export declare function introspectTable(em: EntityManager, table: string): Promise<{
|
|
4
|
+
exists: boolean;
|
|
5
|
+
columns: Set<string>;
|
|
6
|
+
indexes: {
|
|
7
|
+
columnNames: string[];
|
|
8
|
+
unique: boolean;
|
|
9
|
+
primary: boolean;
|
|
10
|
+
}[];
|
|
11
|
+
}>;
|
|
12
|
+
export declare function makeTable(em: EntityManager, table: string): TableOps;
|
|
13
|
+
export declare function validateMigrations(pluginId: string, list: Migration[]): void;
|
|
14
|
+
interface RunArgs {
|
|
15
|
+
em: EntityManager;
|
|
16
|
+
plugins: {
|
|
17
|
+
id: string;
|
|
18
|
+
tables?: string[];
|
|
19
|
+
}[];
|
|
20
|
+
hooks: Record<string, PluginHooks>;
|
|
21
|
+
}
|
|
22
|
+
export declare function runMigrations({ em, plugins, hooks }: RunArgs): Promise<void>;
|
|
23
|
+
export declare function assertSafeRequired(em: EntityManager, entities: EntitySchema[]): Promise<void>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { DatabaseSchema } from '@mikro-orm/sql';
|
|
2
|
+
import { dialectOf } from "./dialect.js";
|
|
3
|
+
function sqlType(col) {
|
|
4
|
+
if (col === 'integer' || col === 'boolean')
|
|
5
|
+
return 'integer';
|
|
6
|
+
if (col === 'real')
|
|
7
|
+
return 'real';
|
|
8
|
+
return 'text';
|
|
9
|
+
}
|
|
10
|
+
function pgType(col) {
|
|
11
|
+
if (col === 'integer')
|
|
12
|
+
return 'integer';
|
|
13
|
+
if (col === 'real')
|
|
14
|
+
return 'double precision';
|
|
15
|
+
if (col === 'boolean')
|
|
16
|
+
return 'boolean';
|
|
17
|
+
if (col === 'date')
|
|
18
|
+
return 'date';
|
|
19
|
+
if (col === 'time')
|
|
20
|
+
return 'time';
|
|
21
|
+
if (col === 'datetime')
|
|
22
|
+
return 'timestamptz';
|
|
23
|
+
return 'text';
|
|
24
|
+
}
|
|
25
|
+
const q = (id) => `"${id.replace(/"/g, '""')}"`;
|
|
26
|
+
export async function introspectTable(em, table) {
|
|
27
|
+
const conn = em.getConnection();
|
|
28
|
+
const platform = em.getPlatform();
|
|
29
|
+
const schema = await DatabaseSchema.create(conn, platform, em.config, undefined, undefined, [table]);
|
|
30
|
+
const t = schema.getTable(table);
|
|
31
|
+
return {
|
|
32
|
+
exists: !!t,
|
|
33
|
+
columns: new Set(t ? t.getColumns().map((c) => c.name) : []),
|
|
34
|
+
indexes: t ? t.getIndexes().map((i) => ({ columnNames: i.columnNames ?? [], unique: !!i.unique, primary: !!i.primary })) : [],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function makeTable(em, table) {
|
|
38
|
+
const conn = em.getConnection();
|
|
39
|
+
const T = q(table);
|
|
40
|
+
return {
|
|
41
|
+
async renameColumn(from, to) {
|
|
42
|
+
const info = await introspectTable(em, table);
|
|
43
|
+
if (!info.exists || !info.columns.has(from))
|
|
44
|
+
return;
|
|
45
|
+
await conn.execute(`ALTER TABLE ${T} RENAME COLUMN ${q(from)} TO ${q(to)}`);
|
|
46
|
+
},
|
|
47
|
+
async fill(column, value, opts) {
|
|
48
|
+
const info = await introspectTable(em, table);
|
|
49
|
+
if (!info.exists || !info.columns.has(column))
|
|
50
|
+
return;
|
|
51
|
+
const where = opts?.where ?? {};
|
|
52
|
+
const keys = Object.keys(where);
|
|
53
|
+
const clauses = keys.map((k) => (where[k] === null ? `${q(k)} IS NULL` : `${q(k)} = ?`));
|
|
54
|
+
const params = [value, ...keys.filter((k) => where[k] !== null).map((k) => where[k])];
|
|
55
|
+
const sql = `UPDATE ${T} SET ${q(column)} = ?` + (clauses.length ? ` WHERE ${clauses.join(' AND ')}` : '');
|
|
56
|
+
await conn.execute(sql, params);
|
|
57
|
+
},
|
|
58
|
+
async changeType(column, newType, opts) {
|
|
59
|
+
const info = await introspectTable(em, table);
|
|
60
|
+
if (!info.exists || !info.columns.has(column))
|
|
61
|
+
return;
|
|
62
|
+
const tmp = `__ct_${column}`;
|
|
63
|
+
const def = opts?.default;
|
|
64
|
+
if (dialectOf(em) === 'sqlite') {
|
|
65
|
+
await conn.execute(`BEGIN`);
|
|
66
|
+
try {
|
|
67
|
+
await conn.execute(`ALTER TABLE ${T} ADD COLUMN ${q(tmp)} ${sqlType(newType)}`);
|
|
68
|
+
if (opts?.transform) {
|
|
69
|
+
const rows = (await conn.execute(`SELECT rowid AS __rid, ${q(column)} AS __old FROM ${T}`));
|
|
70
|
+
for (const r of rows) {
|
|
71
|
+
let next = opts.transform(r.__old);
|
|
72
|
+
if (next === null || next === undefined || (typeof next === 'number' && Number.isNaN(next)))
|
|
73
|
+
next = def ?? null;
|
|
74
|
+
await conn.execute(`UPDATE ${T} SET ${q(tmp)} = ? WHERE rowid = ?`, [next, r.__rid]);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
await conn.execute(`UPDATE ${T} SET ${q(tmp)} = ${q(column)}`);
|
|
79
|
+
if (def !== undefined)
|
|
80
|
+
await conn.execute(`UPDATE ${T} SET ${q(tmp)} = ? WHERE ${q(tmp)} IS NULL`, [def]);
|
|
81
|
+
}
|
|
82
|
+
const savedIndexSql = [];
|
|
83
|
+
const idxList = (await conn.execute(`PRAGMA index_list(${q(table)})`));
|
|
84
|
+
for (const idx of idxList) {
|
|
85
|
+
const info = (await conn.execute(`PRAGMA index_info(${q(idx.name)})`));
|
|
86
|
+
if (!info.some((c) => c.name === column))
|
|
87
|
+
continue;
|
|
88
|
+
const meta = (await conn.execute(`SELECT sql FROM sqlite_master WHERE type='index' AND name=?`, [idx.name]));
|
|
89
|
+
const sql = meta[0]?.sql;
|
|
90
|
+
if (sql == null)
|
|
91
|
+
continue;
|
|
92
|
+
savedIndexSql.push(sql);
|
|
93
|
+
await conn.execute(`DROP INDEX ${q(idx.name)}`);
|
|
94
|
+
}
|
|
95
|
+
await conn.execute(`ALTER TABLE ${T} DROP COLUMN ${q(column)}`);
|
|
96
|
+
await conn.execute(`ALTER TABLE ${T} RENAME COLUMN ${q(tmp)} TO ${q(column)}`);
|
|
97
|
+
for (const sql of savedIndexSql)
|
|
98
|
+
await conn.execute(sql);
|
|
99
|
+
await conn.execute(`COMMIT`);
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
await conn.execute(`ROLLBACK`);
|
|
103
|
+
throw e;
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
await conn.execute(`BEGIN`);
|
|
108
|
+
try {
|
|
109
|
+
await conn.execute(`ALTER TABLE ${T} ADD COLUMN ${q(tmp)} ${pgType(newType)}`);
|
|
110
|
+
if (opts?.transform) {
|
|
111
|
+
const rows = (await conn.execute(`SELECT id AS __rid, ${q(column)} AS __old FROM ${T}`));
|
|
112
|
+
for (const r of rows) {
|
|
113
|
+
let next = opts.transform(r.__old);
|
|
114
|
+
if (next === null || next === undefined || (typeof next === 'number' && Number.isNaN(next)))
|
|
115
|
+
next = def ?? null;
|
|
116
|
+
await conn.execute(`UPDATE ${T} SET ${q(tmp)} = ? WHERE id = ?`, [next, r.__rid]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
await conn.execute(`UPDATE ${T} SET ${q(tmp)} = ${q(column)}::${pgType(newType)}`);
|
|
121
|
+
if (def !== undefined)
|
|
122
|
+
await conn.execute(`UPDATE ${T} SET ${q(tmp)} = ? WHERE ${q(tmp)} IS NULL`, [def]);
|
|
123
|
+
}
|
|
124
|
+
await conn.execute(`ALTER TABLE ${T} DROP COLUMN ${q(column)}`);
|
|
125
|
+
await conn.execute(`ALTER TABLE ${T} RENAME COLUMN ${q(tmp)} TO ${q(column)}`);
|
|
126
|
+
await conn.execute(`COMMIT`);
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
await conn.execute(`ROLLBACK`);
|
|
130
|
+
throw e;
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
export function validateMigrations(pluginId, list) {
|
|
136
|
+
const versions = list.map((m) => m.version).sort((a, b) => a - b);
|
|
137
|
+
versions.forEach((v, i) => {
|
|
138
|
+
if (v !== i + 1) {
|
|
139
|
+
throw new Error(`[migrations] ${pluginId}: versions must be contiguous 1..N without duplicates — got [${list.map((m) => m.version).join(', ')}]`);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
async function anyTableExists(em, tables) {
|
|
144
|
+
for (const t of tables)
|
|
145
|
+
if ((await introspectTable(em, t)).exists)
|
|
146
|
+
return true;
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
export async function runMigrations({ em, plugins, hooks }) {
|
|
150
|
+
const conn = em.getConnection();
|
|
151
|
+
for (const p of plugins) {
|
|
152
|
+
const list = hooks[p.id]?.migrations ?? [];
|
|
153
|
+
if (!list.length)
|
|
154
|
+
continue;
|
|
155
|
+
validateMigrations(p.id, list);
|
|
156
|
+
const journal = (await conn.execute(`SELECT version FROM _migrations WHERE plugin_id = ?`, [p.id]));
|
|
157
|
+
const stored = journal[0]?.version ?? 0;
|
|
158
|
+
const maxV = Math.max(...list.map((m) => m.version));
|
|
159
|
+
if (maxV <= stored)
|
|
160
|
+
continue;
|
|
161
|
+
if (p.tables?.length && !(await anyTableExists(em, p.tables))) {
|
|
162
|
+
await conn.execute(`INSERT INTO _migrations (plugin_id, version) VALUES (?, ?)
|
|
163
|
+
ON CONFLICT(plugin_id) DO UPDATE SET version = excluded.version`, [p.id, maxV]);
|
|
164
|
+
console.log(`[migrations] ${p.id}: fresh DB → stamp v${maxV} (no run)`);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const pending = [...list].filter((m) => m.version > stored).sort((a, b) => a.version - b.version);
|
|
168
|
+
const ctx = { table: (name) => makeTable(em, name) };
|
|
169
|
+
for (const m of pending) {
|
|
170
|
+
await m.up(ctx);
|
|
171
|
+
await conn.execute(`INSERT INTO _migrations (plugin_id, version) VALUES (?, ?)
|
|
172
|
+
ON CONFLICT(plugin_id) DO UPDATE SET version = excluded.version`, [p.id, m.version]);
|
|
173
|
+
}
|
|
174
|
+
if (pending.length)
|
|
175
|
+
console.log(`[migrations] ${p.id}: applied ${pending.length} (→ v${pending[pending.length - 1].version})`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
export async function assertSafeRequired(em, entities) {
|
|
179
|
+
const conn = em.getConnection();
|
|
180
|
+
for (const e of entities) {
|
|
181
|
+
const meta = e.meta;
|
|
182
|
+
const tableName = meta.tableName;
|
|
183
|
+
const props = Object.entries(meta.properties).filter(([, p]) => !p.primary && p.nullable === false && p.default === undefined);
|
|
184
|
+
if (!props.length)
|
|
185
|
+
continue;
|
|
186
|
+
const info = await introspectTable(em, tableName);
|
|
187
|
+
if (!info.exists)
|
|
188
|
+
continue;
|
|
189
|
+
const cnt = (await conn.execute(`SELECT COUNT(*) AS n FROM ${q(tableName)}`));
|
|
190
|
+
if (!(cnt[0]?.n ?? 0))
|
|
191
|
+
continue;
|
|
192
|
+
for (const [col] of props) {
|
|
193
|
+
if (info.columns.has(col))
|
|
194
|
+
continue;
|
|
195
|
+
throw new Error(`[migrations] ${tableName}.${col}: a required field without a default is being added to a non-empty table — add a default in f.* (the field) or a backfill migration`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface MsgLogRow {
|
|
2
|
+
connector: string;
|
|
3
|
+
chatId: string;
|
|
4
|
+
userId: string;
|
|
5
|
+
role: 'user' | 'assistant';
|
|
6
|
+
text: string;
|
|
7
|
+
sessionId: string | null;
|
|
8
|
+
tokensIn: number | null;
|
|
9
|
+
tokensOut: number | null;
|
|
10
|
+
ms: number | null;
|
|
11
|
+
}
|
|
12
|
+
export declare function logMessage(row: MsgLogRow): Promise<void>;
|
package/dist/msg-log.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { getEm } from "./db.js";
|
|
2
|
+
export async function logMessage(row) {
|
|
3
|
+
await getEm().fork().getConnection().execute(`INSERT INTO _orch_msg_log (ts, connector, chat_id, user_id, role, text, session_id, tokens_in, tokens_out, ms)
|
|
4
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [new Date().toISOString(), row.connector, row.chatId, row.userId, row.role, row.text, row.sessionId, row.tokensIn, row.tokensOut, row.ms]);
|
|
5
|
+
}
|
package/dist/mutate.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ModuleDef } from '@coffer-org/sdk/module';
|
|
2
|
+
import { z } from 'zod';
|
|
2
3
|
export interface MutateCtx {
|
|
3
4
|
actor: string;
|
|
4
5
|
}
|
|
@@ -6,6 +7,7 @@ export interface ValidationIssue {
|
|
|
6
7
|
field: string;
|
|
7
8
|
code: string;
|
|
8
9
|
params?: Record<string, unknown>;
|
|
10
|
+
path: (string | number)[];
|
|
9
11
|
}
|
|
10
12
|
export declare class ValidationError extends Error {
|
|
11
13
|
issues: ValidationIssue[];
|
|
@@ -14,9 +16,9 @@ export declare class ValidationError extends Error {
|
|
|
14
16
|
export declare class NotFoundError extends Error {
|
|
15
17
|
constructor();
|
|
16
18
|
}
|
|
19
|
+
export declare function toIssue(i: z.ZodIssue): ValidationIssue;
|
|
17
20
|
export declare function listRecords(entityName: string): Promise<Record<string, unknown>[]>;
|
|
18
|
-
export declare function getRecord(m: ModuleDef, entityName: string, id:
|
|
21
|
+
export declare function getRecord(m: ModuleDef, entityName: string, id: number): Promise<Record<string, unknown> | undefined>;
|
|
19
22
|
export declare function createRecord(m: ModuleDef, entityName: string, input: unknown, ctx: MutateCtx): Promise<Record<string, unknown>>;
|
|
20
|
-
export declare function updateRecord(m: ModuleDef, entityName: string, id:
|
|
21
|
-
export declare function deleteRecord(m: ModuleDef, entityName: string, id:
|
|
22
|
-
//# sourceMappingURL=mutate.d.ts.map
|
|
23
|
+
export declare function updateRecord(m: ModuleDef, entityName: string, id: number, input: unknown, ctx: MutateCtx): Promise<Record<string, unknown>>;
|
|
24
|
+
export declare function deleteRecord(m: ModuleDef, entityName: string, id: number, ctx: MutateCtx): Promise<void>;
|
package/dist/mutate.js
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ЄДИНИЙ ШЛЯХ ЗАПИСУ. Усі мутації проходять тут.
|
|
3
|
-
* validate (Zod) → транзакція → append-only audit (_events).
|
|
4
|
-
*/
|
|
5
|
-
import { randomUUID } from 'node:crypto';
|
|
6
1
|
import { serialize } from '@mikro-orm/core';
|
|
7
|
-
import { buildZodObject, buildZodObjectPartial } from '@coffer-org/sdk/module';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
2
|
+
import { buildZodObject, buildZodObjectPartial, fieldEntries } from '@coffer-org/sdk/module';
|
|
3
|
+
import { isJsonStored, decodeVmsg } from '@coffer-org/sdk/fields';
|
|
4
|
+
import { resolveFlag } from '@coffer-org/sdk/condition';
|
|
5
|
+
import { getEm } from "./db.js";
|
|
6
|
+
import { encodeTemporal, decodeTemporal } from "./temporal.js";
|
|
7
|
+
import { splitCollections, writeCollections, deleteCollections, flattenEmbedded, nestEmbedded, readCollections, } from "./collection-io.js";
|
|
11
8
|
function nowIso() {
|
|
12
9
|
return new Date().toISOString();
|
|
13
10
|
}
|
|
11
|
+
function encodeJson(m, data) {
|
|
12
|
+
const out = { ...data };
|
|
13
|
+
for (const [k, f] of fieldEntries(m.fields)) {
|
|
14
|
+
if (isJsonStored(f) && out[k] !== null && out[k] !== undefined && typeof out[k] === 'object') {
|
|
15
|
+
out[k] = JSON.stringify(out[k]);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
14
20
|
export class ValidationError extends Error {
|
|
15
21
|
issues;
|
|
16
22
|
constructor(issues) {
|
|
@@ -21,25 +27,18 @@ export class ValidationError extends Error {
|
|
|
21
27
|
}
|
|
22
28
|
export class NotFoundError extends Error {
|
|
23
29
|
constructor() {
|
|
24
|
-
super('
|
|
30
|
+
super('Not found');
|
|
25
31
|
this.name = 'NotFoundError';
|
|
26
32
|
}
|
|
27
33
|
}
|
|
28
|
-
function toIssue(i) {
|
|
34
|
+
export function toIssue(i) {
|
|
35
|
+
const path = i.path.map((p) => (typeof p === 'number' ? p : String(p)));
|
|
29
36
|
const field = String(i.path[0] ?? '');
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (typeof m.code === 'string')
|
|
33
|
-
return { field, code: m.code, params: m.params };
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
/* не-vmsg повідомлення від zod */
|
|
37
|
-
}
|
|
38
|
-
return { field, code: 'invalid' };
|
|
37
|
+
const { code, params } = decodeVmsg(i.message);
|
|
38
|
+
return params ? { field, code, params, path } : { field, code, path };
|
|
39
39
|
}
|
|
40
40
|
function writeEvent(em, actor, op, type, recordId, before, after) {
|
|
41
41
|
em.create('_Event', {
|
|
42
|
-
id: randomUUID(),
|
|
43
42
|
ts: nowIso(),
|
|
44
43
|
actor,
|
|
45
44
|
op,
|
|
@@ -49,10 +48,9 @@ function writeEvent(em, actor, op, type, recordId, before, after) {
|
|
|
49
48
|
after: after == null ? null : JSON.stringify(after),
|
|
50
49
|
});
|
|
51
50
|
}
|
|
52
|
-
// ─── READ ─────────────────────────────────────────────────────────────────────
|
|
53
51
|
export async function listRecords(entityName) {
|
|
54
52
|
const fork = getEm().fork();
|
|
55
|
-
const rows = await fork.findAll(entityName);
|
|
53
|
+
const rows = await fork.findAll(entityName, { orderBy: { id: 'asc' } });
|
|
56
54
|
return serialize(rows);
|
|
57
55
|
}
|
|
58
56
|
export async function getRecord(m, entityName, id) {
|
|
@@ -65,27 +63,26 @@ export async function getRecord(m, entityName, id) {
|
|
|
65
63
|
const collections = await readCollections(fork, m, id);
|
|
66
64
|
return { ...nested, ...collections };
|
|
67
65
|
}
|
|
68
|
-
// ─── CREATE ───────────────────────────────────────────────────────────────────
|
|
69
66
|
export async function createRecord(m, entityName, input, ctx) {
|
|
70
|
-
const rawInput = (input ?? {});
|
|
71
67
|
const parsed = buildZodObject(m).safeParse(input);
|
|
72
68
|
if (!parsed.success)
|
|
73
69
|
throw new ValidationError(parsed.error.issues.map(toIssue));
|
|
74
|
-
const id = typeof rawInput.id === 'string' && rawInput.id ? rawInput.id : randomUUID();
|
|
75
70
|
const ts = nowIso();
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
const
|
|
71
|
+
const parsedData = parsed.data;
|
|
72
|
+
const { base, collections } = splitCollections(m, { ...parsedData });
|
|
73
|
+
const data = encodeJson(m, encodeTemporal(m, flattenEmbedded(m, { ...base, created_at: ts, updated_at: ts })));
|
|
74
|
+
let id;
|
|
79
75
|
await getEm()
|
|
80
76
|
.fork()
|
|
81
77
|
.transactional(async (tx) => {
|
|
82
|
-
|
|
78
|
+
const entity = tx.create(entityName, data);
|
|
79
|
+
await tx.flush();
|
|
80
|
+
id = entity.id;
|
|
83
81
|
await writeCollections(tx, m, id, collections);
|
|
84
|
-
writeEvent(tx, ctx.actor, 'create', `${m.vault}/${m.module}`, id, null, { ...
|
|
82
|
+
writeEvent(tx, ctx.actor, 'create', `${m.vault}/${m.module}`, id, null, { ...parsedData, id });
|
|
85
83
|
});
|
|
86
|
-
return { ...
|
|
84
|
+
return { ...parsedData, id, created_at: ts, updated_at: ts };
|
|
87
85
|
}
|
|
88
|
-
// ─── UPDATE ───────────────────────────────────────────────────────────────────
|
|
89
86
|
export async function updateRecord(m, entityName, id, input, ctx) {
|
|
90
87
|
const parsed = buildZodObjectPartial(m).safeParse(input);
|
|
91
88
|
if (!parsed.success)
|
|
@@ -102,16 +99,30 @@ export async function updateRecord(m, entityName, id, input, ctx) {
|
|
|
102
99
|
const ts = nowIso();
|
|
103
100
|
const { base, collections } = splitCollections(m, { ...parsed.data });
|
|
104
101
|
const merged = { ...existing, ...base };
|
|
105
|
-
const
|
|
102
|
+
const reqIssues = [];
|
|
103
|
+
for (const [key, f] of fieldEntries(m.fields)) {
|
|
104
|
+
if (f.virtual || f.required === true)
|
|
105
|
+
continue;
|
|
106
|
+
if (key.includes('__'))
|
|
107
|
+
continue;
|
|
108
|
+
if (!resolveFlag(f.required, merged))
|
|
109
|
+
continue;
|
|
110
|
+
const v = merged[key];
|
|
111
|
+
if (v == null || v === '' || (Array.isArray(v) && v.length === 0))
|
|
112
|
+
reqIssues.push({ field: key, code: 'required', path: [key] });
|
|
113
|
+
}
|
|
114
|
+
if (reqIssues.length)
|
|
115
|
+
throw new ValidationError(reqIssues);
|
|
116
|
+
const dbRow = encodeJson(m, encodeTemporal(m, flattenEmbedded(m, { ...merged, updated_at: ts })));
|
|
106
117
|
await tx.upsert(entityName, dbRow);
|
|
107
118
|
await writeCollections(tx, m, id, collections);
|
|
108
|
-
const
|
|
119
|
+
const allCollections = await readCollections(tx, m, id);
|
|
120
|
+
const after = { ...merged, ...allCollections, updated_at: ts };
|
|
109
121
|
writeEvent(tx, ctx.actor, 'update', `${m.vault}/${m.module}`, id, existing, after);
|
|
110
122
|
result = after;
|
|
111
123
|
});
|
|
112
124
|
return result;
|
|
113
125
|
}
|
|
114
|
-
// ─── DELETE ───────────────────────────────────────────────────────────────────
|
|
115
126
|
export async function deleteRecord(m, entityName, id, ctx) {
|
|
116
127
|
await getEm()
|
|
117
128
|
.fork()
|
|
@@ -125,4 +136,3 @@ export async function deleteRecord(m, entityName, id, ctx) {
|
|
|
125
136
|
writeEvent(tx, ctx.actor, 'delete', `${m.vault}/${m.module}`, id, existing, null);
|
|
126
137
|
});
|
|
127
138
|
}
|
|
128
|
-
//# sourceMappingURL=mutate.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PluginManifest } from '@coffer-org/sdk/plugin';
|
|
2
|
-
import type { PluginHooks } from './plugin-hooks.
|
|
2
|
+
import type { PluginHooks } from './plugin-hooks.ts';
|
|
3
3
|
export interface PluginAssetPaths {
|
|
4
4
|
schema: string;
|
|
5
5
|
web?: string;
|
|
@@ -10,9 +10,6 @@ export interface PluginAssetUrls {
|
|
|
10
10
|
web?: string;
|
|
11
11
|
css?: string;
|
|
12
12
|
}
|
|
13
|
-
/** Maps a plugin's published `coffer` asset paths to host-served URLs.
|
|
14
|
-
* The relative path's basename is fixed by convention (schema.js/web.js/web.css);
|
|
15
|
-
* the actual file is read from node_modules by the static handler. */
|
|
16
13
|
export declare function resolveAssetUrls(id: string, coffer: PluginAssetPaths): PluginAssetUrls;
|
|
17
14
|
export interface PluginAssetRecord {
|
|
18
15
|
id: string;
|
|
@@ -24,13 +21,5 @@ export interface PluginAssetRecord {
|
|
|
24
21
|
css?: string;
|
|
25
22
|
}
|
|
26
23
|
export declare function discoverPluginAssets(): Promise<PluginAssetRecord[]>;
|
|
27
|
-
/** Усі знайдені плагіни. Порядок неважливий — composeRegistry резолвить за dependsOn. */
|
|
28
24
|
export declare function discoverPlugins(): Promise<PluginManifest[]>;
|
|
29
|
-
/**
|
|
30
|
-
* Збирає серверні хуки наявних плагінів. Плагін із runtime-входом
|
|
31
|
-
* (package.json `coffer.server` або `exports["./runtime"]`) може експортувати
|
|
32
|
-
* `serverHooks: PluginHooks`. Імпорт — через ЗМІННИЙ специфікатор, тож tsc НЕ тягне
|
|
33
|
-
* plugin-src у серверну програму (розрив циклу server↔плагіни).
|
|
34
|
-
*/
|
|
35
25
|
export declare function loadServerHooks(): Promise<Record<string, PluginHooks>>;
|
|
36
|
-
//# sourceMappingURL=plugin-discovery.d.ts.map
|
package/dist/plugin-discovery.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
6
9
|
import { readdir, readFile } from 'node:fs/promises';
|
|
7
10
|
import { join } from 'node:path';
|
|
8
11
|
import { pathToFileURL } from 'node:url';
|
|
@@ -15,7 +18,6 @@ async function pkgNames(nm) {
|
|
|
15
18
|
}
|
|
16
19
|
}
|
|
17
20
|
catch {
|
|
18
|
-
/* немає @coffer-скоупу — ок */
|
|
19
21
|
}
|
|
20
22
|
try {
|
|
21
23
|
for (const d of await readdir(nm)) {
|
|
@@ -24,13 +26,9 @@ async function pkgNames(nm) {
|
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
28
|
catch {
|
|
27
|
-
/* немає node_modules — ок */
|
|
28
29
|
}
|
|
29
30
|
return names;
|
|
30
31
|
}
|
|
31
|
-
/** Maps a plugin's published `coffer` asset paths to host-served URLs.
|
|
32
|
-
* The relative path's basename is fixed by convention (schema.js/web.js/web.css);
|
|
33
|
-
* the actual file is read from node_modules by the static handler. */
|
|
34
32
|
export function resolveAssetUrls(id, coffer) {
|
|
35
33
|
const base = `/plugins/${id}`;
|
|
36
34
|
const out = { schema: `${base}/schema.js` };
|
|
@@ -50,7 +48,7 @@ export async function discoverPluginAssets() {
|
|
|
50
48
|
if (!pkg.coffer?.schema)
|
|
51
49
|
continue;
|
|
52
50
|
const spec = pathToFileURL(join(nm, name, pkg.coffer.schema)).href;
|
|
53
|
-
const manifest = (await import(spec)).default;
|
|
51
|
+
const manifest = (await import(__rewriteRelativeImportExtension(spec))).default;
|
|
54
52
|
if (!manifest?.id)
|
|
55
53
|
continue;
|
|
56
54
|
const urls = resolveAssetUrls(manifest.id, pkg.coffer);
|
|
@@ -75,23 +73,19 @@ async function loadManifest(nm, name) {
|
|
|
75
73
|
const pkg = JSON.parse(await readFile(join(nm, name, 'package.json'), 'utf8'));
|
|
76
74
|
const schemaRel = pkg?.coffer?.schema;
|
|
77
75
|
const spec = schemaRel ? pathToFileURL(join(nm, name, schemaRel)).href : name;
|
|
78
|
-
const mod = (await import(spec));
|
|
76
|
+
const mod = (await import(__rewriteRelativeImportExtension(spec)));
|
|
79
77
|
const manifest = mod.default;
|
|
80
78
|
if (manifest && typeof manifest.id === 'string')
|
|
81
79
|
return manifest;
|
|
82
|
-
console.warn(`[discovery] ${name}:
|
|
80
|
+
console.warn(`[discovery] ${name}: no valid default manifest, skipping`);
|
|
83
81
|
return null;
|
|
84
82
|
}
|
|
85
83
|
catch (e) {
|
|
86
|
-
console.error(`[discovery] ${name}:
|
|
84
|
+
console.error(`[discovery] ${name}: import failed, skipping — ${e.message}`);
|
|
87
85
|
return null;
|
|
88
86
|
}
|
|
89
87
|
}
|
|
90
|
-
/** Усі знайдені плагіни. Порядок неважливий — composeRegistry резолвить за dependsOn. */
|
|
91
88
|
export async function discoverPlugins() {
|
|
92
|
-
// Скануємо node_modules коренем інстансу (cwd): сервер запускається з кореня,
|
|
93
|
-
// npm workspaces хостить пакети у корене́вий node_modules. Запуск із packages/server
|
|
94
|
-
// дивився б у локальний node_modules і не побачив би хостовані плагіни.
|
|
95
89
|
const nm = join(process.cwd(), 'node_modules');
|
|
96
90
|
const names = await pkgNames(nm);
|
|
97
91
|
const manifests = [];
|
|
@@ -102,12 +96,6 @@ export async function discoverPlugins() {
|
|
|
102
96
|
}
|
|
103
97
|
return manifests;
|
|
104
98
|
}
|
|
105
|
-
/**
|
|
106
|
-
* Збирає серверні хуки наявних плагінів. Плагін із runtime-входом
|
|
107
|
-
* (package.json `coffer.server` або `exports["./runtime"]`) може експортувати
|
|
108
|
-
* `serverHooks: PluginHooks`. Імпорт — через ЗМІННИЙ специфікатор, тож tsc НЕ тягне
|
|
109
|
-
* plugin-src у серверну програму (розрив циклу server↔плагіни).
|
|
110
|
-
*/
|
|
111
99
|
export async function loadServerHooks() {
|
|
112
100
|
const nm = join(process.cwd(), 'node_modules');
|
|
113
101
|
const names = await pkgNames(nm);
|
|
@@ -119,19 +107,18 @@ export async function loadServerHooks() {
|
|
|
119
107
|
const hasRuntime = Boolean(serverRel) || Boolean(pkg?.exports?.['./runtime']);
|
|
120
108
|
if (!hasRuntime)
|
|
121
109
|
continue;
|
|
122
|
-
const manifest = (await import(name)).default;
|
|
110
|
+
const manifest = (await import(__rewriteRelativeImportExtension(name))).default;
|
|
123
111
|
const id = manifest?.id;
|
|
124
112
|
if (!id)
|
|
125
113
|
continue;
|
|
126
114
|
const spec = serverRel ? pathToFileURL(join(nm, name, serverRel)).href : `${name}/runtime`;
|
|
127
|
-
const mod = (await import(spec));
|
|
115
|
+
const mod = (await import(__rewriteRelativeImportExtension(spec)));
|
|
128
116
|
if (mod.serverHooks)
|
|
129
117
|
hooks[id] = mod.serverHooks;
|
|
130
118
|
}
|
|
131
119
|
catch (e) {
|
|
132
|
-
console.error(`[discovery] ${name}:
|
|
120
|
+
console.error(`[discovery] ${name}: server hooks not loaded — ${e.message}`);
|
|
133
121
|
}
|
|
134
122
|
}
|
|
135
123
|
return hooks;
|
|
136
124
|
}
|
|
137
|
-
//# sourceMappingURL=plugin-discovery.js.map
|